From e9dd38c3a870efb1d35b68e268cf803267412af8 Mon Sep 17 00:00:00 2001 From: val Date: Wed, 30 Nov 2022 17:27:28 +0100 Subject: [PATCH] commit fonctionne ou pas ? --- .dependencies | 34 + .editorconfig | 17 + .github/FUNDING.yml | 8 + .gitignore | 55 + .gitmodules | 9 + .htaccess | 80 + .phan/config.php | 44 + .phan/internal_stubs/Redis.phan_php | 5153 + .phan/internal_stubs/memcache.phan_php | 460 + .phan/internal_stubs/memcached.phan_php | 1308 + .travis.yml | 96 + CHANGELOG.md | 3741 + CODE_OF_CONDUCT.md | 133 + CONTRIBUTING.md | 138 + LICENSE.txt | 21 + README.md | 149 + SECURITY.md | 21 + assets/.gitkeep | 1 + backup/.gitkeep | 0 bin/composer.phar | Bin 0 -> 2205196 bytes bin/gpm | 54 + bin/grav | 50 + bin/plugin | 51 + cache/.gitkeep | 0 codeception.yml | 16 + composer.json | 122 + composer.lock | 6702 ++ fixperms.sh | 7 + images/.gitkeep | 0 index.php | 65 + logs/.gitkeep | 0 now.json | 4 + robots.txt | 13 + system/assets/debugger/clockwork.css | 2 + system/assets/debugger/clockwork.js | 3 + system/assets/debugger/phpdebugbar.css | 70 + system/assets/grav.png | Bin 0 -> 1612 bytes system/assets/jquery/jquery-2.1.4.min.js | 4 + system/assets/jquery/jquery-2.x.min.js | 4 + system/assets/jquery/jquery-3.x.min.js | 2 + system/assets/responsive-overlays/1x.png | Bin 0 -> 3238 bytes system/assets/responsive-overlays/2x.png | Bin 0 -> 7593 bytes system/assets/responsive-overlays/3x.png | Bin 0 -> 13002 bytes system/assets/responsive-overlays/4x.png | Bin 0 -> 15545 bytes system/assets/responsive-overlays/unknown.png | Bin 0 -> 5241 bytes system/assets/whoops.css | 19 + system/blueprints/config/backups.yaml | 125 + system/blueprints/config/media.yaml | 5 + system/blueprints/config/scheduler.yaml | 78 + system/blueprints/config/security.yaml | 119 + system/blueprints/config/site.yaml | 124 + system/blueprints/config/streams.yaml | 8 + system/blueprints/config/system.yaml | 1772 + system/blueprints/flex/accounts.yaml | 8 + system/blueprints/flex/configure/compat.yaml | 17 + system/blueprints/flex/pages.yaml | 212 + system/blueprints/flex/shared/configure.yaml | 70 + system/blueprints/flex/user-accounts.yaml | 142 + system/blueprints/flex/user-groups.yaml | 123 + system/blueprints/pages/default.yaml | 369 + system/blueprints/pages/external.yaml | 52 + system/blueprints/pages/modular.yaml | 36 + .../blueprints/pages/partials/security.yaml | 71 + system/blueprints/pages/root.yaml | 16 + system/blueprints/user/account.yaml | 140 + system/blueprints/user/account_new.yaml | 18 + system/blueprints/user/group.yaml | 55 + system/blueprints/user/group_new.yaml | 23 + system/config/backups.yaml | 15 + system/config/media.yaml | 218 + system/config/mime.yaml | 1986 + system/config/permissions.yaml | 53 + system/config/security.yaml | 39 + system/config/site.yaml | 35 + system/config/system.yaml | 223 + system/defines.php | 101 + system/images/media/thumb-3dm.png | Bin 0 -> 3013 bytes system/images/media/thumb-3ds.png | Bin 0 -> 3116 bytes system/images/media/thumb-3g2.png | Bin 0 -> 3006 bytes system/images/media/thumb-3gp.png | Bin 0 -> 2853 bytes system/images/media/thumb-7z.png | Bin 0 -> 1648 bytes system/images/media/thumb-aac.png | Bin 0 -> 3218 bytes system/images/media/thumb-ai.png | Bin 0 -> 1595 bytes system/images/media/thumb-aif.png | Bin 0 -> 1691 bytes system/images/media/thumb-apk.png | Bin 0 -> 2557 bytes system/images/media/thumb-app.png | Bin 0 -> 2360 bytes system/images/media/thumb-asf.png | Bin 0 -> 2635 bytes system/images/media/thumb-asp.png | Bin 0 -> 2913 bytes system/images/media/thumb-aspx.png | Bin 0 -> 3726 bytes system/images/media/thumb-asx.png | Bin 0 -> 3287 bytes system/images/media/thumb-avi.png | Bin 0 -> 2448 bytes system/images/media/thumb-bak.png | Bin 0 -> 2814 bytes system/images/media/thumb-bat.png | Bin 0 -> 2182 bytes system/images/media/thumb-bin.png | Bin 0 -> 1841 bytes system/images/media/thumb-bmp.png | Bin 0 -> 2573 bytes system/images/media/thumb-cab.png | Bin 0 -> 3056 bytes system/images/media/thumb-cad.png | Bin 0 -> 3021 bytes system/images/media/thumb-cdr.png | Bin 0 -> 2713 bytes system/images/media/thumb-cer.png | Bin 0 -> 2367 bytes system/images/media/thumb-cfg.png | Bin 0 -> 2764 bytes system/images/media/thumb-cfm.png | Bin 0 -> 2589 bytes system/images/media/thumb-cgi.png | Bin 0 -> 2626 bytes system/images/media/thumb-com.png | Bin 0 -> 3292 bytes system/images/media/thumb-cpl.png | Bin 0 -> 2174 bytes system/images/media/thumb-cpp.png | Bin 0 -> 2425 bytes system/images/media/thumb-crx.png | Bin 0 -> 2965 bytes system/images/media/thumb-csr.png | Bin 0 -> 3158 bytes system/images/media/thumb-css.png | Bin 0 -> 3235 bytes system/images/media/thumb-csv.png | Bin 0 -> 3317 bytes system/images/media/thumb-cue.png | Bin 0 -> 2271 bytes system/images/media/thumb-cur.png | Bin 0 -> 2716 bytes system/images/media/thumb-dat.png | Bin 0 -> 2188 bytes system/images/media/thumb-db.png | Bin 0 -> 1964 bytes system/images/media/thumb-dbf.png | Bin 0 -> 1947 bytes system/images/media/thumb-dds.png | Bin 0 -> 2815 bytes system/images/media/thumb-dem.png | Bin 0 -> 2215 bytes system/images/media/thumb-dll.png | Bin 0 -> 1352 bytes system/images/media/thumb-dmg.png | Bin 0 -> 3064 bytes system/images/media/thumb-dmp.png | Bin 0 -> 2527 bytes system/images/media/thumb-doc.png | Bin 0 -> 3051 bytes system/images/media/thumb-docx.png | Bin 0 -> 3865 bytes system/images/media/thumb-drv.png | Bin 0 -> 2728 bytes system/images/media/thumb-dtd.png | Bin 0 -> 1949 bytes system/images/media/thumb-dwg.png | Bin 0 -> 3530 bytes system/images/media/thumb-dxf.png | Bin 0 -> 2392 bytes system/images/media/thumb-elf.png | Bin 0 -> 663 bytes system/images/media/thumb-eot.png | Bin 0 -> 2002 bytes system/images/media/thumb-eps.png | Bin 0 -> 2276 bytes system/images/media/thumb-exe.png | Bin 0 -> 1813 bytes system/images/media/thumb-fla.png | Bin 0 -> 1724 bytes system/images/media/thumb-flv.png | Bin 0 -> 1720 bytes system/images/media/thumb-fnt.png | Bin 0 -> 1254 bytes system/images/media/thumb-fon.png | Bin 0 -> 2402 bytes system/images/media/thumb-gam.png | Bin 0 -> 3203 bytes system/images/media/thumb-gbr.png | Bin 0 -> 2849 bytes system/images/media/thumb-ged.png | Bin 0 -> 2269 bytes system/images/media/thumb-gif.png | Bin 0 -> 1751 bytes system/images/media/thumb-gpx.png | Bin 0 -> 2972 bytes system/images/media/thumb-gz.png | Bin 0 -> 2134 bytes system/images/media/thumb-gzip.png | Bin 0 -> 2451 bytes system/images/media/thumb-hqz.png | Bin 0 -> 2604 bytes system/images/media/thumb-html.png | Bin 0 -> 1740 bytes system/images/media/thumb-icns.png | Bin 0 -> 3212 bytes system/images/media/thumb-ico.png | Bin 0 -> 2769 bytes system/images/media/thumb-ics.png | Bin 0 -> 2787 bytes system/images/media/thumb-iff.png | Bin 0 -> 601 bytes system/images/media/thumb-indd.png | Bin 0 -> 2475 bytes system/images/media/thumb-iso.png | Bin 0 -> 2864 bytes system/images/media/thumb-jar.png | Bin 0 -> 2583 bytes system/images/media/thumb-jpg.png | Bin 0 -> 2435 bytes system/images/media/thumb-js.png | Bin 0 -> 2046 bytes system/images/media/thumb-json.png | Bin 0 -> 7818 bytes system/images/media/thumb-jsp.png | Bin 0 -> 2498 bytes system/images/media/thumb-key.png | Bin 0 -> 2130 bytes system/images/media/thumb-kml.png | Bin 0 -> 2346 bytes system/images/media/thumb-kmz.png | Bin 0 -> 2701 bytes system/images/media/thumb-lnk.png | Bin 0 -> 1971 bytes system/images/media/thumb-log.png | Bin 0 -> 2762 bytes system/images/media/thumb-lua.png | Bin 0 -> 2117 bytes system/images/media/thumb-m3u.png | Bin 0 -> 2909 bytes system/images/media/thumb-m4a.png | Bin 0 -> 2754 bytes system/images/media/thumb-m4v.png | Bin 0 -> 2738 bytes system/images/media/thumb-max.png | Bin 0 -> 3213 bytes system/images/media/thumb-mdb.png | Bin 0 -> 2691 bytes system/images/media/thumb-mdf.png | Bin 0 -> 2243 bytes system/images/media/thumb-mid.png | Bin 0 -> 2199 bytes system/images/media/thumb-mim.png | Bin 0 -> 2284 bytes system/images/media/thumb-mov.png | Bin 0 -> 3221 bytes system/images/media/thumb-mp3.png | Bin 0 -> 2801 bytes system/images/media/thumb-mp4.png | Bin 0 -> 2221 bytes system/images/media/thumb-mpa.png | Bin 0 -> 2734 bytes system/images/media/thumb-mpe.png | Bin 0 -> 1971 bytes system/images/media/thumb-mpg.png | Bin 0 -> 2811 bytes system/images/media/thumb-msg.png | Bin 0 -> 3319 bytes system/images/media/thumb-msi.png | Bin 0 -> 2594 bytes system/images/media/thumb-nes.png | Bin 0 -> 2320 bytes system/images/media/thumb-obj.png | Bin 0 -> 2716 bytes system/images/media/thumb-odb.png | Bin 0 -> 2912 bytes system/images/media/thumb-odc.png | Bin 0 -> 3239 bytes system/images/media/thumb-odf.png | Bin 0 -> 2496 bytes system/images/media/thumb-odg.png | Bin 0 -> 3069 bytes system/images/media/thumb-odi.png | Bin 0 -> 2453 bytes system/images/media/thumb-odp.png | Bin 0 -> 2871 bytes system/images/media/thumb-ods.png | Bin 0 -> 3257 bytes system/images/media/thumb-odt.png | Bin 0 -> 2414 bytes system/images/media/thumb-odx.png | Bin 0 -> 3133 bytes system/images/media/thumb-ogg.png | Bin 0 -> 3577 bytes system/images/media/thumb-pct.png | Bin 0 -> 2248 bytes system/images/media/thumb-pdb.png | Bin 0 -> 2354 bytes system/images/media/thumb-pdf.png | Bin 0 -> 1823 bytes system/images/media/thumb-pif.png | Bin 0 -> 1103 bytes system/images/media/thumb-pkg.png | Bin 0 -> 2660 bytes system/images/media/thumb-pl.png | Bin 0 -> 1066 bytes system/images/media/thumb-png.png | Bin 0 -> 2530 bytes system/images/media/thumb-pps.png | Bin 0 -> 2497 bytes system/images/media/thumb-ppt.png | Bin 0 -> 1573 bytes system/images/media/thumb-pptx.png | Bin 0 -> 2560 bytes system/images/media/thumb-ps.png | Bin 0 -> 2285 bytes system/images/media/thumb-psd.png | Bin 0 -> 2613 bytes system/images/media/thumb-pub.png | Bin 0 -> 2137 bytes system/images/media/thumb-py.png | Bin 0 -> 1776 bytes system/images/media/thumb-ra.png | Bin 0 -> 2223 bytes system/images/media/thumb-rar.png | Bin 0 -> 2695 bytes system/images/media/thumb-raw.png | Bin 0 -> 3433 bytes system/images/media/thumb-rm.png | Bin 0 -> 2181 bytes system/images/media/thumb-rom.png | Bin 0 -> 3088 bytes system/images/media/thumb-rpm.png | Bin 0 -> 2566 bytes system/images/media/thumb-rss.png | Bin 0 -> 3091 bytes system/images/media/thumb-rtf.png | Bin 0 -> 1364 bytes system/images/media/thumb-sav.png | Bin 0 -> 3204 bytes system/images/media/thumb-sdf.png | Bin 0 -> 2444 bytes system/images/media/thumb-sql.png | Bin 0 -> 2929 bytes system/images/media/thumb-srt.png | Bin 0 -> 2366 bytes system/images/media/thumb-svg.png | Bin 0 -> 3359 bytes system/images/media/thumb-swf.png | Bin 0 -> 3181 bytes system/images/media/thumb-sys.png | Bin 0 -> 3128 bytes system/images/media/thumb-tar.png | Bin 0 -> 2165 bytes system/images/media/thumb-tex.png | Bin 0 -> 1716 bytes system/images/media/thumb-tga.png | Bin 0 -> 2655 bytes system/images/media/thumb-thm.png | Bin 0 -> 1666 bytes system/images/media/thumb-tiff.png | Bin 0 -> 690 bytes system/images/media/thumb-tmp.png | Bin 0 -> 2040 bytes system/images/media/thumb-ttf.png | Bin 0 -> 615 bytes system/images/media/thumb-txt.png | Bin 0 -> 1853 bytes system/images/media/thumb-uue.png | Bin 0 -> 1632 bytes system/images/media/thumb-vb.png | Bin 0 -> 2074 bytes system/images/media/thumb-vcd.png | Bin 0 -> 3040 bytes system/images/media/thumb-vcf.png | Bin 0 -> 2581 bytes system/images/media/thumb-wav.png | Bin 0 -> 3601 bytes system/images/media/thumb-webm.png | Bin 0 -> 3843 bytes system/images/media/thumb-wma.png | Bin 0 -> 3552 bytes system/images/media/thumb-wmv.png | Bin 0 -> 3789 bytes system/images/media/thumb-woff.png | Bin 0 -> 3421 bytes system/images/media/thumb-woff2.png | Bin 0 -> 3927 bytes system/images/media/thumb-wpd.png | Bin 0 -> 3127 bytes system/images/media/thumb-wps.png | Bin 0 -> 3368 bytes system/images/media/thumb-wsf.png | Bin 0 -> 3028 bytes system/images/media/thumb-xls.png | Bin 0 -> 2562 bytes system/images/media/thumb-xlsx.png | Bin 0 -> 3481 bytes system/images/media/thumb-xml.png | Bin 0 -> 2557 bytes system/images/media/thumb-yuv.png | Bin 0 -> 2741 bytes system/images/media/thumb-zip.png | Bin 0 -> 1628 bytes system/images/media/thumb.png | Bin 0 -> 1200 bytes system/images/watermark.png | Bin 0 -> 95789 bytes system/install.php | 15 + system/languages/ar.yaml | 93 + system/languages/bg.yaml | 62 + system/languages/ca.yaml | 87 + system/languages/cs.yaml | 147 + system/languages/da.yaml | 90 + system/languages/de.yaml | 147 + system/languages/el.yaml | 144 + system/languages/en.yaml | 121 + system/languages/es.yaml | 107 + system/languages/et.yaml | 104 + system/languages/eu.yaml | 62 + system/languages/fa.yaml | 62 + system/languages/fi.yaml | 134 + system/languages/fr.yaml | 128 + system/languages/gl.yaml | 147 + system/languages/he.yaml | 63 + system/languages/hr.yaml | 76 + system/languages/hu.yaml | 97 + system/languages/id.yaml | 147 + system/languages/is.yaml | 80 + system/languages/it.yaml | 147 + system/languages/ja.yaml | 81 + system/languages/ko.yaml | 63 + system/languages/lt.yaml | 78 + system/languages/mn.yaml | 147 + system/languages/nb.yaml | 4 + system/languages/nl.yaml | 144 + system/languages/no.yaml | 82 + system/languages/pl.yaml | 100 + system/languages/pt.yaml | 147 + system/languages/ro.yaml | 96 + system/languages/ru.yaml | 104 + system/languages/si.yaml | 9 + system/languages/sk.yaml | 144 + system/languages/sl.yaml | 62 + system/languages/sr.yaml | 144 + system/languages/sv.yaml | 100 + system/languages/th.yaml | 56 + system/languages/tr.yaml | 100 + system/languages/uk.yaml | 63 + system/languages/vi.yaml | 63 + system/languages/zh-cn.yaml | 144 + system/languages/zh-tw.yaml | 77 + system/languages/zh.yaml | 144 + system/pages/notfound.md | 6 + system/router.php | 55 + system/src/Grav/Common/Assets.php | 451 + system/src/Grav/Common/Assets/BaseAsset.php | 268 + system/src/Grav/Common/Assets/Css.php | 52 + system/src/Grav/Common/Assets/InlineCss.php | 44 + system/src/Grav/Common/Assets/InlineJs.php | 44 + system/src/Grav/Common/Assets/Js.php | 48 + system/src/Grav/Common/Assets/Pipeline.php | 285 + .../Common/Assets/Traits/AssetUtilsTrait.php | 210 + .../Assets/Traits/LegacyAssetsTrait.php | 137 + .../Assets/Traits/TestingAssetsTrait.php | 341 + system/src/Grav/Common/Backup/Backups.php | 322 + system/src/Grav/Common/Browser.php | 153 + system/src/Grav/Common/Cache.php | 690 + system/src/Grav/Common/Composer.php | 67 + .../src/Grav/Common/Config/CompiledBase.php | 269 + .../Grav/Common/Config/CompiledBlueprints.php | 131 + .../src/Grav/Common/Config/CompiledConfig.php | 114 + .../Grav/Common/Config/CompiledLanguages.php | 83 + system/src/Grav/Common/Config/Config.php | 156 + .../Grav/Common/Config/ConfigFileFinder.php | 273 + system/src/Grav/Common/Config/Languages.php | 107 + system/src/Grav/Common/Config/Setup.php | 422 + system/src/Grav/Common/Data/Blueprint.php | 593 + .../src/Grav/Common/Data/BlueprintSchema.php | 422 + system/src/Grav/Common/Data/Blueprints.php | 121 + system/src/Grav/Common/Data/Data.php | 342 + system/src/Grav/Common/Data/DataInterface.php | 84 + system/src/Grav/Common/Data/Validation.php | 1214 + .../Grav/Common/Data/ValidationException.php | 67 + system/src/Grav/Common/Debugger.php | 1140 + system/src/Grav/Common/Errors/BareHandler.php | 33 + system/src/Grav/Common/Errors/Errors.php | 85 + .../Grav/Common/Errors/Resources/error.css | 52 + .../Common/Errors/Resources/layout.html.php | 30 + .../Grav/Common/Errors/SimplePageHandler.php | 122 + .../src/Grav/Common/Errors/SystemFacade.php | 46 + system/src/Grav/Common/File/CompiledFile.php | 122 + .../src/Grav/Common/File/CompiledJsonFile.php | 33 + .../Grav/Common/File/CompiledMarkdownFile.php | 21 + .../src/Grav/Common/File/CompiledYamlFile.php | 21 + .../src/Grav/Common/Filesystem/Archiver.php | 108 + system/src/Grav/Common/Filesystem/Folder.php | 541 + .../RecursiveDirectoryFilterIterator.php | 82 + .../RecursiveFolderFilterIterator.php | 55 + .../Grav/Common/Filesystem/ZipArchiver.php | 135 + .../src/Grav/Common/Flex/FlexCollection.php | 28 + system/src/Grav/Common/Flex/FlexIndex.php | 29 + system/src/Grav/Common/Flex/FlexObject.php | 74 + .../Flex/Traits/FlexCollectionTrait.php | 51 + .../Common/Flex/Traits/FlexCommonTrait.php | 54 + .../Grav/Common/Flex/Traits/FlexGravTrait.php | 74 + .../Common/Flex/Traits/FlexIndexTrait.php | 20 + .../Common/Flex/Traits/FlexObjectTrait.php | 62 + .../Flex/Types/Generic/GenericCollection.php | 24 + .../Flex/Types/Generic/GenericIndex.php | 24 + .../Flex/Types/Generic/GenericObject.php | 22 + .../Flex/Types/Pages/PageCollection.php | 818 + .../Common/Flex/Types/Pages/PageIndex.php | 1163 + .../Common/Flex/Types/Pages/PageObject.php | 730 + .../Flex/Types/Pages/Storage/PageStorage.php | 700 + .../Types/Pages/Traits/PageContentTrait.php | 75 + .../Types/Pages/Traits/PageLegacyTrait.php | 236 + .../Types/Pages/Traits/PageRoutableTrait.php | 122 + .../Types/Pages/Traits/PageTranslateTrait.php | 108 + .../Types/UserGroups/UserGroupCollection.php | 56 + .../Flex/Types/UserGroups/UserGroupIndex.php | 24 + .../Flex/Types/UserGroups/UserGroupObject.php | 121 + .../Types/Users/Storage/UserFileStorage.php | 47 + .../Types/Users/Storage/UserFolderStorage.php | 37 + .../Users/Traits/UserObjectLegacyTrait.php | 93 + .../Flex/Types/Users/UserCollection.php | 135 + .../Common/Flex/Types/Users/UserIndex.php | 204 + .../Common/Flex/Types/Users/UserObject.php | 951 + system/src/Grav/Common/Form/FormFlash.php | 106 + .../Grav/Common/GPM/AbstractCollection.php | 41 + .../GPM/Common/AbstractPackageCollection.php | 50 + .../Common/GPM/Common/CachedCollection.php | 43 + system/src/Grav/Common/GPM/Common/Package.php | 95 + system/src/Grav/Common/GPM/GPM.php | 1267 + system/src/Grav/Common/GPM/Installer.php | 543 + system/src/Grav/Common/GPM/Licenses.php | 116 + .../GPM/Local/AbstractPackageCollection.php | 34 + system/src/Grav/Common/GPM/Local/Package.php | 51 + system/src/Grav/Common/GPM/Local/Packages.php | 29 + system/src/Grav/Common/GPM/Local/Plugins.php | 33 + system/src/Grav/Common/GPM/Local/Themes.php | 33 + .../GPM/Remote/AbstractPackageCollection.php | 81 + .../src/Grav/Common/GPM/Remote/GravCore.php | 151 + system/src/Grav/Common/GPM/Remote/Package.php | 65 + .../src/Grav/Common/GPM/Remote/Packages.php | 34 + system/src/Grav/Common/GPM/Remote/Plugins.php | 32 + system/src/Grav/Common/GPM/Remote/Themes.php | 32 + system/src/Grav/Common/GPM/Response.php | 3 + system/src/Grav/Common/GPM/Upgrader.php | 138 + system/src/Grav/Common/Getters.php | 165 + system/src/Grav/Common/Grav.php | 786 + system/src/Grav/Common/GravTrait.php | 34 + system/src/Grav/Common/HTTP/Client.php | 130 + system/src/Grav/Common/HTTP/Response.php | 96 + system/src/Grav/Common/Helpers/Base32.php | 141 + system/src/Grav/Common/Helpers/Excerpts.php | 196 + system/src/Grav/Common/Helpers/Exif.php | 48 + system/src/Grav/Common/Helpers/LogViewer.php | 167 + system/src/Grav/Common/Helpers/Truncator.php | 344 + system/src/Grav/Common/Helpers/YamlLinter.php | 121 + system/src/Grav/Common/Inflector.php | 357 + system/src/Grav/Common/Iterator.php | 261 + system/src/Grav/Common/Language/Language.php | 662 + .../Grav/Common/Language/LanguageCodes.php | 244 + system/src/Grav/Common/Markdown/Parsedown.php | 42 + .../Grav/Common/Markdown/ParsedownExtra.php | 46 + .../Common/Markdown/ParsedownGravTrait.php | 302 + .../Media/Interfaces/AudioMediaInterface.php | 25 + .../Interfaces/ImageManipulateInterface.php | 120 + .../Media/Interfaces/ImageMediaInterface.php | 17 + .../Interfaces/MediaCollectionInterface.php | 115 + .../Media/Interfaces/MediaFileInterface.php | 53 + .../Media/Interfaces/MediaInterface.php | 17 + .../Media/Interfaces/MediaLinkInterface.php | 17 + .../Media/Interfaces/MediaObjectInterface.php | 236 + .../Media/Interfaces/MediaPlayerInterface.php | 56 + .../Media/Interfaces/MediaUploadInterface.php | 73 + .../Media/Interfaces/VideoMediaInterface.php | 32 + .../Common/Media/Traits/AudioMediaTrait.php | 53 + .../Common/Media/Traits/ImageLoadingTrait.php | 37 + .../Common/Media/Traits/ImageMediaTrait.php | 428 + .../Common/Media/Traits/MediaFileTrait.php | 139 + .../Common/Media/Traits/MediaObjectTrait.php | 609 + .../Common/Media/Traits/MediaPlayerTrait.php | 113 + .../Grav/Common/Media/Traits/MediaTrait.php | 153 + .../Common/Media/Traits/MediaUploadTrait.php | 674 + .../Common/Media/Traits/StaticResizeTrait.php | 40 + .../Media/Traits/ThumbnailMediaTrait.php | 149 + .../Common/Media/Traits/VideoMediaTrait.php | 68 + system/src/Grav/Common/Page/Collection.php | 707 + system/src/Grav/Common/Page/Header.php | 37 + .../Interfaces/PageCollectionInterface.php | 283 + .../Page/Interfaces/PageContentInterface.php | 257 + .../Page/Interfaces/PageFormInterface.php | 33 + .../Common/Page/Interfaces/PageInterface.php | 25 + .../Page/Interfaces/PageLegacyInterface.php | 475 + .../Page/Interfaces/PageRoutableInterface.php | 180 + .../Interfaces/PageTranslateInterface.php | 33 + .../Page/Interfaces/PagesSourceInterface.php | 56 + .../Grav/Common/Page/Markdown/Excerpts.php | 343 + system/src/Grav/Common/Page/Media.php | 239 + .../Grav/Common/Page/Medium/AbstractMedia.php | 343 + .../Grav/Common/Page/Medium/AudioMedium.php | 36 + .../Grav/Common/Page/Medium/GlobalMedia.php | 149 + .../src/Grav/Common/Page/Medium/ImageFile.php | 211 + .../Grav/Common/Page/Medium/ImageMedium.php | 477 + system/src/Grav/Common/Page/Medium/Link.php | 101 + system/src/Grav/Common/Page/Medium/Medium.php | 130 + .../Grav/Common/Page/Medium/MediumFactory.php | 216 + .../Common/Page/Medium/ParsedownHtmlTrait.php | 44 + .../Page/Medium/RenderableInterface.php | 41 + .../Common/Page/Medium/StaticImageMedium.php | 40 + .../Common/Page/Medium/StaticResizeTrait.php | 24 + .../Page/Medium/ThumbnailImageMedium.php | 21 + .../Grav/Common/Page/Medium/VideoMedium.php | 36 + system/src/Grav/Common/Page/Page.php | 2900 + system/src/Grav/Common/Page/Pages.php | 2202 + .../Grav/Common/Page/Traits/PageFormTrait.php | 126 + system/src/Grav/Common/Page/Types.php | 178 + system/src/Grav/Common/Plugin.php | 448 + system/src/Grav/Common/Plugins.php | 330 + .../Common/Processors/AssetsProcessor.php | 41 + .../Common/Processors/BackupsProcessor.php | 41 + .../Processors/DebuggerAssetsProcessor.php | 40 + .../Processors/Events/RequestHandlerEvent.php | 82 + .../Common/Processors/InitializeProcessor.php | 460 + .../Grav/Common/Processors/PagesProcessor.php | 114 + .../Common/Processors/PluginsProcessor.php | 41 + .../Grav/Common/Processors/ProcessorBase.php | 70 + .../Common/Processors/ProcessorInterface.php | 20 + .../Common/Processors/RenderProcessor.php | 71 + .../Common/Processors/RequestProcessor.php | 65 + .../Common/Processors/SchedulerProcessor.php | 42 + .../Grav/Common/Processors/TasksProcessor.php | 71 + .../Common/Processors/ThemesProcessor.php | 40 + .../Grav/Common/Processors/TwigProcessor.php | 40 + system/src/Grav/Common/Scheduler/Cron.php | 577 + .../Grav/Common/Scheduler/IntervalTrait.php | 404 + system/src/Grav/Common/Scheduler/Job.php | 566 + .../src/Grav/Common/Scheduler/Scheduler.php | 437 + system/src/Grav/Common/Security.php | 252 + .../Service/AccountsServiceProvider.php | 157 + .../Common/Service/AssetsServiceProvider.php | 32 + .../Common/Service/BackupsServiceProvider.php | 35 + .../Common/Service/ConfigServiceProvider.php | 206 + .../Common/Service/ErrorServiceProvider.php | 30 + .../Service/FilesystemServiceProvider.php | 32 + .../Common/Service/FlexServiceProvider.php | 117 + .../Service/InflectorServiceProvider.php | 32 + .../Common/Service/LoggerServiceProvider.php | 42 + .../Common/Service/OutputServiceProvider.php | 39 + .../Common/Service/PagesServiceProvider.php | 139 + .../Common/Service/RequestServiceProvider.php | 103 + .../Service/SchedulerServiceProvider.php | 32 + .../Common/Service/SessionServiceProvider.php | 133 + .../Common/Service/StreamsServiceProvider.php | 56 + .../Common/Service/TaskServiceProvider.php | 55 + system/src/Grav/Common/Session.php | 190 + system/src/Grav/Common/Taxonomy.php | 176 + system/src/Grav/Common/Theme.php | 87 + system/src/Grav/Common/Themes.php | 417 + .../Common/Twig/Exception/TwigException.php | 19 + .../Twig/Extension/FilesystemExtension.php | 391 + .../Common/Twig/Extension/GravExtension.php | 1641 + .../Grav/Common/Twig/Node/TwigNodeCache.php | 58 + .../Common/Twig/Node/TwigNodeMarkdown.php | 52 + .../Grav/Common/Twig/Node/TwigNodeRender.php | 83 + .../Grav/Common/Twig/Node/TwigNodeScript.php | 104 + .../Grav/Common/Twig/Node/TwigNodeStyle.php | 103 + .../Grav/Common/Twig/Node/TwigNodeSwitch.php | 88 + .../Grav/Common/Twig/Node/TwigNodeThrow.php | 52 + .../Common/Twig/Node/TwigNodeTryCatch.php | 67 + .../Twig/TokenParser/TwigTokenParserCache.php | 71 + .../TokenParser/TwigTokenParserMarkdown.php | 59 + .../TokenParser/TwigTokenParserRender.php | 74 + .../TokenParser/TwigTokenParserScript.php | 120 + .../Twig/TokenParser/TwigTokenParserStyle.php | 119 + .../TokenParser/TwigTokenParserSwitch.php | 132 + .../Twig/TokenParser/TwigTokenParserThrow.php | 55 + .../TokenParser/TwigTokenParserTryCatch.php | 81 + system/src/Grav/Common/Twig/Twig.php | 553 + .../Common/Twig/TwigClockworkDataSource.php | 58 + .../Grav/Common/Twig/TwigClockworkDumper.php | 72 + .../src/Grav/Common/Twig/TwigEnvironment.php | 21 + system/src/Grav/Common/Twig/TwigExtension.php | 21 + .../Grav/Common/Twig/WriteCacheFileTrait.php | 56 + system/src/Grav/Common/Uri.php | 1524 + system/src/Grav/Common/User/Access.php | 52 + .../src/Grav/Common/User/Authentication.php | 61 + system/src/Grav/Common/User/DataUser/User.php | 327 + .../Common/User/DataUser/UserCollection.php | 162 + system/src/Grav/Common/User/Group.php | 172 + .../User/Interfaces/AuthorizeInterface.php | 26 + .../Interfaces/UserCollectionInterface.php | 40 + .../User/Interfaces/UserGroupInterface.php | 18 + .../Common/User/Interfaces/UserInterface.php | 189 + .../src/Grav/Common/User/Traits/UserTrait.php | 187 + system/src/Grav/Common/User/User.php | 144 + system/src/Grav/Common/Utils.php | 2116 + system/src/Grav/Common/Yaml.php | 65 + .../Grav/Console/Application/Application.php | 106 + .../CommandLoader/PluginCommandLoader.php | 95 + .../Console/Application/GpmApplication.php | 42 + .../Console/Application/GravApplication.php | 52 + .../Console/Application/PluginApplication.php | 116 + system/src/Grav/Console/Cli/BackupCommand.php | 138 + system/src/Grav/Console/Cli/CleanCommand.php | 411 + .../Grav/Console/Cli/ClearCacheCommand.php | 104 + .../src/Grav/Console/Cli/ComposerCommand.php | 64 + .../src/Grav/Console/Cli/InstallCommand.php | 302 + .../src/Grav/Console/Cli/LogViewerCommand.php | 96 + .../Grav/Console/Cli/NewProjectCommand.php | 75 + .../Cli/PageSystemValidatorCommand.php | 299 + .../src/Grav/Console/Cli/SandboxCommand.php | 342 + .../src/Grav/Console/Cli/SchedulerCommand.php | 225 + .../src/Grav/Console/Cli/SecurityCommand.php | 102 + system/src/Grav/Console/Cli/ServerCommand.php | 154 + .../Grav/Console/Cli/YamlLinterCommand.php | 124 + system/src/Grav/Console/ConsoleCommand.php | 46 + system/src/Grav/Console/ConsoleTrait.php | 338 + .../Grav/Console/Gpm/DirectInstallCommand.php | 322 + system/src/Grav/Console/Gpm/IndexCommand.php | 335 + system/src/Grav/Console/Gpm/InfoCommand.php | 191 + .../src/Grav/Console/Gpm/InstallCommand.php | 726 + .../Grav/Console/Gpm/SelfupgradeCommand.php | 344 + .../src/Grav/Console/Gpm/UninstallCommand.php | 312 + system/src/Grav/Console/Gpm/UpdateCommand.php | 289 + .../src/Grav/Console/Gpm/VersionCommand.php | 125 + system/src/Grav/Console/GpmCommand.php | 68 + system/src/Grav/Console/GravCommand.php | 52 + .../Grav/Console/Plugin/PluginListCommand.php | 69 + .../Grav/Console/TerminalObjects/Table.php | 38 + system/src/Grav/Events/FlexRegisterEvent.php | 45 + .../Grav/Events/PermissionsRegisterEvent.php | 45 + system/src/Grav/Events/PluginsLoadedEvent.php | 53 + system/src/Grav/Events/SessionStartEvent.php | 36 + system/src/Grav/Framework/Acl/Access.php | 236 + system/src/Grav/Framework/Acl/Action.php | 203 + system/src/Grav/Framework/Acl/Permissions.php | 244 + .../Grav/Framework/Acl/PermissionsReader.php | 187 + .../Framework/Acl/RecursiveActionIterator.php | 60 + .../Grav/Framework/Cache/AbstractCache.php | 32 + .../Framework/Cache/Adapter/ChainCache.php | 206 + .../Framework/Cache/Adapter/DoctrineCache.php | 114 + .../Framework/Cache/Adapter/FileCache.php | 255 + .../Framework/Cache/Adapter/MemoryCache.php | 83 + .../Framework/Cache/Adapter/SessionCache.php | 107 + .../Grav/Framework/Cache/CacheInterface.php | 71 + .../src/Grav/Framework/Cache/CacheTrait.php | 376 + .../Cache/Exception/CacheException.php | 21 + .../Exception/InvalidArgumentException.php | 20 + .../Collection/AbstractFileCollection.php | 236 + .../Collection/AbstractIndexCollection.php | 550 + .../Collection/AbstractLazyCollection.php | 87 + .../Framework/Collection/ArrayCollection.php | 104 + .../Collection/CollectionInterface.php | 68 + .../Framework/Collection/FileCollection.php | 97 + .../Collection/FileCollectionInterface.php | 33 + .../Grav/Framework/Compat/Serializable.php | 47 + .../Framework/ContentBlock/ContentBlock.php | 302 + .../ContentBlock/ContentBlockInterface.php | 90 + .../Grav/Framework/ContentBlock/HtmlBlock.php | 398 + .../ContentBlock/HtmlBlockInterface.php | 95 + .../Traits/ControllerResponseTrait.php | 306 + system/src/Grav/Framework/DI/Container.php | 35 + .../src/Grav/Framework/File/AbstractFile.php | 441 + system/src/Grav/Framework/File/CsvFile.php | 31 + system/src/Grav/Framework/File/DataFile.php | 78 + system/src/Grav/Framework/File/File.php | 44 + .../File/Formatter/AbstractFormatter.php | 117 + .../Framework/File/Formatter/CsvFormatter.php | 169 + .../File/Formatter/FormatterInterface.php | 12 + .../Framework/File/Formatter/IniFormatter.php | 67 + .../File/Formatter/JsonFormatter.php | 165 + .../File/Formatter/MarkdownFormatter.php | 160 + .../File/Formatter/SerializeFormatter.php | 98 + .../File/Formatter/YamlFormatter.php | 127 + system/src/Grav/Framework/File/IniFile.php | 31 + .../Interfaces/FileFormatterInterface.php | 72 + .../File/Interfaces/FileInterface.php | 180 + system/src/Grav/Framework/File/JsonFile.php | 31 + .../src/Grav/Framework/File/MarkdownFile.php | 31 + system/src/Grav/Framework/File/YamlFile.php | 31 + .../Grav/Framework/Filesystem/Filesystem.php | 340 + .../Interfaces/FilesystemInterface.php | 82 + system/src/Grav/Framework/Flex/Flex.php | 332 + .../Grav/Framework/Flex/FlexCollection.php | 712 + .../src/Grav/Framework/Flex/FlexDirectory.php | 1077 + .../Grav/Framework/Flex/FlexDirectoryForm.php | 503 + system/src/Grav/Framework/Flex/FlexForm.php | 604 + .../src/Grav/Framework/Flex/FlexFormFlash.php | 130 + system/src/Grav/Framework/Flex/FlexIndex.php | 905 + system/src/Grav/Framework/Flex/FlexObject.php | 1290 + .../Interfaces/FlexAuthorizeInterface.php | 33 + .../Interfaces/FlexCollectionInterface.php | 144 + .../Flex/Interfaces/FlexCommonInterface.php | 79 + .../Interfaces/FlexDirectoryFormInterface.php | 27 + .../Interfaces/FlexDirectoryInterface.php | 223 + .../Flex/Interfaces/FlexFormInterface.php | 46 + .../Flex/Interfaces/FlexIndexInterface.php | 64 + .../Flex/Interfaces/FlexInterface.php | 98 + .../Interfaces/FlexObjectFormInterface.php | 27 + .../Flex/Interfaces/FlexObjectInterface.php | 210 + .../Flex/Interfaces/FlexStorageInterface.php | 138 + .../Interfaces/FlexTranslateInterface.php | 51 + .../Flex/Pages/FlexPageCollection.php | 203 + .../Framework/Flex/Pages/FlexPageIndex.php | 48 + .../Framework/Flex/Pages/FlexPageObject.php | 495 + .../Flex/Pages/Traits/PageAuthorsTrait.php | 249 + .../Flex/Pages/Traits/PageContentTrait.php | 840 + .../Flex/Pages/Traits/PageLegacyTrait.php | 1119 + .../Flex/Pages/Traits/PageRoutableTrait.php | 550 + .../Flex/Pages/Traits/PageTranslateTrait.php | 283 + .../Storage/AbstractFilesystemStorage.php | 228 + .../Framework/Flex/Storage/FileStorage.php | 159 + .../Framework/Flex/Storage/FolderStorage.php | 708 + .../Framework/Flex/Storage/SimpleStorage.php | 506 + .../Flex/Traits/FlexAuthorizeTrait.php | 126 + .../Framework/Flex/Traits/FlexMediaTrait.php | 520 + .../Flex/Traits/FlexRelatedDirectoryTrait.php | 59 + system/src/Grav/Framework/Form/FormFlash.php | 572 + .../src/Grav/Framework/Form/FormFlashFile.php | 237 + .../Form/Interfaces/FormFactoryInterface.php | 42 + .../Form/Interfaces/FormFlashInterface.php | 174 + .../Form/Interfaces/FormInterface.php | 187 + .../Grav/Framework/Form/Traits/FormTrait.php | 870 + .../Framework/Interfaces/RenderInterface.php | 38 + .../Logger/Processors/UserProcessor.php | 34 + .../Interfaces/MediaCollectionInterface.php | 17 + .../Media/Interfaces/MediaInterface.php | 37 + .../Interfaces/MediaManipulationInterface.php | 33 + .../Media/Interfaces/MediaObjectInterface.php | 17 + system/src/Grav/Framework/Mime/MimeTypes.php | 107 + .../Object/Access/ArrayAccessTrait.php | 66 + .../Object/Access/NestedArrayAccessTrait.php | 66 + .../Access/NestedPropertyCollectionTrait.php | 120 + .../Object/Access/NestedPropertyTrait.php | 180 + .../Object/Access/OverloadedPropertyTrait.php | 62 + .../src/Grav/Framework/Object/ArrayObject.php | 30 + .../Object/Base/ObjectCollectionTrait.php | 364 + .../Framework/Object/Base/ObjectTrait.php | 200 + .../Collection/ObjectExpressionVisitor.php | 239 + .../NestedObjectCollectionInterface.php | 64 + .../Interfaces/NestedObjectInterface.php | 60 + .../Interfaces/ObjectCollectionInterface.php | 126 + .../Object/Interfaces/ObjectInterface.php | 63 + .../src/Grav/Framework/Object/LazyObject.php | 33 + .../Framework/Object/ObjectCollection.php | 128 + .../src/Grav/Framework/Object/ObjectIndex.php | 266 + .../Object/Property/ArrayPropertyTrait.php | 115 + .../Object/Property/LazyPropertyTrait.php | 115 + .../Object/Property/MixedPropertyTrait.php | 121 + .../Object/Property/ObjectPropertyTrait.php | 213 + .../Grav/Framework/Object/PropertyObject.php | 30 + .../Pagination/AbstractPagination.php | 427 + .../Pagination/AbstractPaginationPage.php | 78 + .../Interfaces/PaginationInterface.php | 103 + .../Interfaces/PaginationPageInterface.php | 47 + .../Grav/Framework/Pagination/Pagination.php | 32 + .../Framework/Pagination/PaginationPage.php | 26 + .../src/Grav/Framework/Psr7/AbstractUri.php | 411 + system/src/Grav/Framework/Psr7/Request.php | 34 + system/src/Grav/Framework/Psr7/Response.php | 264 + .../src/Grav/Framework/Psr7/ServerRequest.php | 374 + system/src/Grav/Framework/Psr7/Stream.php | 43 + .../Psr7/Traits/MessageDecoratorTrait.php | 140 + .../Psr7/Traits/RequestDecoratorTrait.php | 112 + .../Psr7/Traits/ResponseDecoratorTrait.php | 82 + .../Traits/ServerRequestDecoratorTrait.php | 176 + .../Psr7/Traits/StreamDecoratorTrait.php | 152 + .../Traits/UploadedFileDecoratorTrait.php | 73 + .../Psr7/Traits/UriDecorationTrait.php | 188 + .../src/Grav/Framework/Psr7/UploadedFile.php | 70 + system/src/Grav/Framework/Psr7/Uri.php | 138 + .../Exception/InvalidArgumentException.php | 49 + .../Exception/NotFoundException.php | 45 + .../Exception/NotHandledException.php | 20 + .../Exception/PageExpiredException.php | 32 + .../Exception/RequestException.php | 102 + .../RequestHandler/Middlewares/Exceptions.php | 80 + .../RequestHandler/RequestHandler.php | 70 + .../Traits/RequestHandlerTrait.php | 64 + system/src/Grav/Framework/Route/Route.php | 450 + .../src/Grav/Framework/Route/RouteFactory.php | 236 + .../Session/Exceptions/SessionException.php | 20 + .../src/Grav/Framework/Session/Messages.php | 134 + system/src/Grav/Framework/Session/Session.php | 536 + .../Framework/Session/SessionInterface.php | 152 + system/src/Grav/Framework/Uri/Uri.php | 216 + system/src/Grav/Framework/Uri/UriFactory.php | 171 + .../src/Grav/Framework/Uri/UriPartsFilter.php | 145 + system/src/Grav/Installer/Install.php | 392 + .../src/Grav/Installer/InstallException.php | 29 + system/src/Grav/Installer/VersionUpdate.php | 82 + system/src/Grav/Installer/VersionUpdater.php | 133 + system/src/Grav/Installer/Versions.php | 329 + system/src/Grav/Installer/YamlUpdater.php | 430 + .../Installer/updates/1.7.0_2020-11-20_1.php | 24 + .../DeferredExtension/DeferredBlockNode.php | 43 + .../DeferredExtension/DeferredExtension.php | 72 + .../DeferredExtensionNode.php | 27 + .../Twig/DeferredExtension/DeferredNode.php | 27 + .../DeferredExtension/DeferredNodeVisitor.php | 49 + .../DeferredNodeVisitorCompat.php | 49 + .../DeferredExtension/DeferredTokenParser.php | 77 + system/templates/default.html.twig | 4 + system/templates/external.html.twig | 1 + system/templates/flex/404.html.twig | 4 + .../flex/_default/collection/debug.html.twig | 5 + .../flex/_default/object/debug.html.twig | 4 + system/templates/modular/default.html.twig | 4 + system/templates/partials/messages.html.twig | 14 + system/templates/partials/metadata.html.twig | 3 + tests/_bootstrap.php | 35 + tests/_support/AcceptanceTester.php | 26 + tests/_support/FunctionalTester.php | 26 + tests/_support/Helper/Acceptance.php | 10 + tests/_support/Helper/Functional.php | 10 + tests/_support/Helper/Unit.php | 80 + tests/_support/UnitTester.php | 26 + tests/acceptance.suite.yml | 12 + tests/acceptance/_bootstrap.php | 2 + .../01.item1-1/01.item1-1-1/default.md | 10 + .../01.item1-1/02.item1-1-2/default.md | 10 + .../01.item1-1/03.item1-1-3/default.md | 10 + .../user/pages/01.item1/01.item1-1/default.md | 10 + .../02.item1-2/01.item1-2-1/default.md | 10 + .../02.item1-2/02.item1-2-2/default.md | 10 + .../02.item1-2/03.item1-2-3/default.md | 10 + .../user/pages/01.item1/02.item1-2/default.md | 10 + .../03.item1-3/01.item1-3-1/default.md | 10 + .../03.item1-3/02.item1-3-2/default.md | 10 + .../03.item1-3/03.item1-3-3/default.md | 10 + .../user/pages/01.item1/03.item1-3/default.md | 10 + .../user/pages/01.item1/default.md | 10 + .../user/pages/01.item1/existing-file.zip | Bin 0 -> 451 bytes .../user/pages/01.item1/home-cache-image.jpg | Bin 0 -> 156699 bytes .../user/pages/01.item1/home-sample-image.jpg | Bin 0 -> 156699 bytes .../01.item2-1/01.item2-1-1/default.md | 10 + .../01.item2-1/02.item2-1-2/default.md | 10 + .../01.item2-1/03.item2-1-3/default.md | 10 + .../user/pages/02.item2/01.item2-1/default.md | 10 + .../02.item2-2/01.item2-2-1/default.md | 10 + .../02.item2-2/02.item2-2-2/default.md | 10 + .../02.item2-2/03.item2-2-3/default.md | 10 + .../pages/02.item2/02.item2-2/cache-image.jpg | Bin 0 -> 156699 bytes .../user/pages/02.item2/02.item2-2/default.md | 10 + .../02.item2/02.item2-2/existing-file.zip | Bin 0 -> 451 bytes .../02.item2/02.item2-2/sample-image.jpg | Bin 0 -> 156699 bytes .../03.item2-3/01.item2-3-1/default.md | 10 + .../03.item2-3/02.item2-3-2/default.md | 10 + .../03.item2-3/03.item2-3-3/default.md | 10 + .../user/pages/02.item2/03.item2-3/default.md | 10 + .../user/pages/02.item2/default.md | 10 + .../01.item3-1/01.item3-1-1/default.md | 14 + .../01.item3-1/02.item3-1-2/default.md | 10 + .../01.item3-1/03.item3-1-3/default.md | 10 + .../user/pages/03.item3/01.item3-1/default.md | 10 + .../02.item3-2/01.item3-2-1/default.md | 10 + .../02.item3-2/02.item3-2-2/default.md | 10 + .../02.item3-2/03.item3-2-3/default.md | 10 + .../user/pages/03.item3/02.item3-2/default.md | 10 + .../03.item3-3/01.item3-3-1/default.md | 10 + .../03.item3-3/02.item3-3-2/default.md | 10 + .../03.item3-3/03.item3-3-3/default.md | 10 + .../user/pages/03.item3/03.item3-3/default.md | 10 + .../user/pages/03.item3/default.md | 10 + .../simple-site/user/pages/01.home/default.md | 10 + .../simple-site/user/pages/02.blog/blog.md | 10 + .../user/pages/02.blog/post-one/item.md | 10 + .../user/pages/02.blog/post-two/item.md | 10 + .../user/pages/03.about/default.md | 10 + .../single-pages/01.simple-page/default.md | 5 + tests/functional.suite.yml | 11 + .../Grav/Console/DirectInstallCommandTest.php | 40 + tests/functional/_bootstrap.php | 2 + tests/phpstan/phpstan-bootstrap.php | 7 + tests/phpstan/phpstan.neon | 107 + tests/phpstan/plugins-bootstrap.php | 41 + tests/phpstan/plugins.neon | 20 + tests/unit.suite.yml | 9 + tests/unit/Grav/Common/AssetsTest.php | 778 + tests/unit/Grav/Common/BrowserTest.php | 48 + tests/unit/Grav/Common/ComposerTest.php | 32 + tests/unit/Grav/Common/GPM/GPMTest.php | 323 + .../unit/Grav/Common/Helpers/ExcerptsTest.php | 85 + tests/unit/Grav/Common/InflectorTest.php | 147 + .../Common/Language/LanguageCodesTest.php | 24 + .../Grav/Common/Markdown/ParsedownTest.php | 757 + tests/unit/Grav/Common/Page/PagesTest.php | 281 + .../Grav/Common/Twig/TwigExtensionTest.php | 208 + tests/unit/Grav/Common/UriTest.php | 1153 + tests/unit/Grav/Common/UtilsTest.php | 526 + .../Grav/Console/Gpm/InstallCommandTest.php | 28 + .../File/Formatter/CsvFormatterTest.php | 47 + .../Framework/Filesystem/FilesystemTest.php | 322 + tests/unit/_bootstrap.php | 4 + tmp/.gitkeep | 0 user/config/backups.yaml | 0 user/config/groups.yaml | 38 + user/config/media.yaml | 0 user/config/plugins/imgsrcset.yaml | 11 + user/config/plugins/lazy-image.yaml | 2 + user/config/plugins/sitemap.yaml | 7 + user/config/scheduler.yaml | 0 user/config/site.yaml | 17 + user/config/streams.yaml | 0 user/config/system.yaml | 155 + user/config/versions.yaml | 8 + user/data/.gitkeep | 0 user/localhost/config/plugins/aura.yaml | 16 + .../config/plugins/automagic-images.yaml | 23 + user/localhost/config/plugins/lazy-image.yaml | 2 + user/localhost/config/site.yaml | 19 + user/localhost/config/system.yaml | 228 + user/plugins/.gitkeep | 0 user/plugins/admin/.editorconfig | 17 + user/plugins/admin/.gitattributes | 8 + user/plugins/admin/.github/FUNDING.yml | 8 + user/plugins/admin/.gitignore | 16 + user/plugins/admin/CHANGELOG.md | 2427 + user/plugins/admin/CONTRIBUTING.md | 1 + user/plugins/admin/LICENSE | 21 + user/plugins/admin/README.md | 152 + user/plugins/admin/UPGRADE.md | 6 + user/plugins/admin/admin.php | 1347 + user/plugins/admin/admin.yaml | 85 + user/plugins/admin/assets/admin-dashboard.png | Bin 0 -> 241372 bytes user/plugins/admin/blueprints.yaml | 787 + .../admin/blueprints/admin/pages/copy.yaml | 43 + .../blueprints/admin/pages/modular_new.yaml | 52 + .../blueprints/admin/pages/modular_raw.yaml | 104 + .../admin/blueprints/admin/pages/move.yaml | 5 + .../admin/blueprints/admin/pages/new.yaml | 62 + .../blueprints/admin/pages/new_folder.yaml | 31 + .../admin/blueprints/admin/pages/raw.yaml | 104 + .../blueprints/admin/pages/root_raw.yaml | 34 + .../admin/blueprints/config/media.yaml | 36 + user/plugins/admin/classes/plugin/Admin.php | 2493 + .../classes/plugin/AdminBaseController.php | 1167 + .../admin/classes/plugin/AdminController.php | 3044 + .../admin/classes/plugin/AdminForm.php | 182 + .../admin/classes/plugin/AdminFormFactory.php | 44 + .../plugin/Controllers/AbstractController.php | 407 + .../plugin/Controllers/AdminController.php | 359 + .../Controllers/Login/LoginController.php | 630 + user/plugins/admin/classes/plugin/Gpm.php | 435 + .../admin/classes/plugin/Popularity.php | 303 + user/plugins/admin/classes/plugin/Router.php | 72 + .../classes/plugin/Routers/LoginRouter.php | 93 + .../admin/classes/plugin/ScssCompiler.php | 55 + .../plugins/admin/classes/plugin/ScssList.php | 52 + user/plugins/admin/classes/plugin/Themes.php | 22 + .../plugin/Twig/AdminTwigExtension.php | 131 + user/plugins/admin/classes/plugin/Utils.php | 54 + .../admin/classes/plugin/WhiteLabel.php | 93 + user/plugins/admin/codeception.yml | 18 + user/plugins/admin/composer.json | 57 + user/plugins/admin/composer.lock | 3756 + user/plugins/admin/hebe.json | 15 + user/plugins/admin/languages/ar.yaml | 307 + user/plugins/admin/languages/bg.yaml | 356 + user/plugins/admin/languages/bn.yaml | 4 + user/plugins/admin/languages/br.yaml | 592 + user/plugins/admin/languages/ca.yaml | 643 + user/plugins/admin/languages/cs.yaml | 1112 + user/plugins/admin/languages/cy.yaml | 89 + user/plugins/admin/languages/da.yaml | 760 + user/plugins/admin/languages/de.yaml | 965 + user/plugins/admin/languages/el.yaml | 710 + user/plugins/admin/languages/en.yaml | 1124 + user/plugins/admin/languages/eo.yaml | 766 + user/plugins/admin/languages/es.yaml | 989 + user/plugins/admin/languages/et.yaml | 896 + user/plugins/admin/languages/eu.yaml | 181 + user/plugins/admin/languages/fa.yaml | 878 + user/plugins/admin/languages/fi.yaml | 259 + user/plugins/admin/languages/fr.yaml | 1112 + user/plugins/admin/languages/gl.yaml | 867 + user/plugins/admin/languages/he.yaml | 410 + user/plugins/admin/languages/hr.yaml | 600 + user/plugins/admin/languages/hu.yaml | 777 + user/plugins/admin/languages/id.yaml | 708 + user/plugins/admin/languages/it.yaml | 1112 + user/plugins/admin/languages/ja.yaml | 759 + user/plugins/admin/languages/ko.yaml | 107 + user/plugins/admin/languages/ku.yaml | 121 + user/plugins/admin/languages/lt.yaml | 359 + user/plugins/admin/languages/mn.yaml | 1112 + user/plugins/admin/languages/nl.yaml | 853 + user/plugins/admin/languages/no.yaml | 730 + user/plugins/admin/languages/pl.yaml | 795 + user/plugins/admin/languages/pt.yaml | 620 + user/plugins/admin/languages/ro.yaml | 1106 + user/plugins/admin/languages/ru.yaml | 1103 + user/plugins/admin/languages/si.yaml | 98 + user/plugins/admin/languages/sk.yaml | 548 + user/plugins/admin/languages/sl.yaml | 242 + user/plugins/admin/languages/sr.yaml | 805 + user/plugins/admin/languages/sv.yaml | 670 + user/plugins/admin/languages/th.yaml | 688 + user/plugins/admin/languages/tlh.yaml | 8 + user/plugins/admin/languages/tr.yaml | 862 + user/plugins/admin/languages/uk.yaml | 600 + user/plugins/admin/languages/vi.yaml | 452 + user/plugins/admin/languages/zh-cn.yaml | 1112 + user/plugins/admin/languages/zh-tw.yaml | 718 + user/plugins/admin/languages/zh.yaml | 1112 + user/plugins/admin/pages/admin/ajax.md | 8 + user/plugins/admin/pages/admin/backup.md | 9 + user/plugins/admin/pages/admin/cache.md | 10 + user/plugins/admin/pages/admin/changelog.md | 9 + user/plugins/admin/pages/admin/config.md | 7 + user/plugins/admin/pages/admin/dashboard.md | 8 + user/plugins/admin/pages/admin/denied.md | 5 + user/plugins/admin/pages/admin/error.md | 10 + user/plugins/admin/pages/admin/feed.md | 8 + user/plugins/admin/pages/admin/forgot.md | 19 + user/plugins/admin/pages/admin/info.md | 9 + user/plugins/admin/pages/admin/installer.md | 8 + user/plugins/admin/pages/admin/login.md | 40 + user/plugins/admin/pages/admin/media.md | 9 + .../admin/pages/admin/notifications.md | 8 + .../plugins/admin/pages/admin/pages-filter.md | 9 + user/plugins/admin/pages/admin/pages.md | 8 + user/plugins/admin/pages/admin/plugins.md | 8 + user/plugins/admin/pages/admin/preview.md | 8 + user/plugins/admin/pages/admin/register.md | 63 + user/plugins/admin/pages/admin/reset.md | 24 + user/plugins/admin/pages/admin/site.md | 9 + user/plugins/admin/pages/admin/statistics.md | 8 + user/plugins/admin/pages/admin/system.md | 9 + user/plugins/admin/pages/admin/themes.md | 8 + user/plugins/admin/pages/admin/tools.md | 7 + user/plugins/admin/pages/admin/update.md | 9 + user/plugins/admin/pages/admin/user.md | 9 + user/plugins/admin/permissions.yaml | 48 + user/plugins/admin/presets.yaml | 370 + user/plugins/admin/tests/_bootstrap.php | 11 + .../admin/tests/_support/Helper/Unit.php | 80 + .../admin/tests/_support/UnitTester.php | 26 + user/plugins/admin/tests/unit.suite.yml | 9 + user/plugins/admin/tests/unit/_bootstrap.php | 3 + .../tests/unit/classes/controllerTest.php | 42 + user/plugins/admin/themes/grav/.babelrc | 3 + user/plugins/admin/themes/grav/.eslintrc | 166 + .../admin/themes/grav/app/dashboard/backup.js | 42 + .../admin/themes/grav/app/dashboard/cache.js | 49 + .../admin/themes/grav/app/dashboard/chart.js | 138 + .../admin/themes/grav/app/dashboard/index.js | 12 + .../admin/themes/grav/app/dashboard/update.js | 1 + .../grav/app/forms/fields/acl-picker.js | 89 + .../themes/grav/app/forms/fields/array.js | 205 + .../grav/app/forms/fields/collections.js | 261 + .../grav/app/forms/fields/colorpicker.js | 579 + .../themes/grav/app/forms/fields/cron.js | 85 + .../themes/grav/app/forms/fields/datetime.js | 63 + .../themes/grav/app/forms/fields/editor.js | 237 + .../grav/app/forms/fields/editor/buttons.js | 416 + .../grav/app/forms/fields/filepicker.js | 144 + .../themes/grav/app/forms/fields/files.js | 390 + .../themes/grav/app/forms/fields/folder.js | 20 + .../grav/app/forms/fields/iconpicker.js | 298 + .../grav/app/forms/fields/indeterminate.js | 52 + .../themes/grav/app/forms/fields/index.js | 75 + .../grav/app/forms/fields/mediapicker.js | 51 + .../grav/app/forms/fields/multilevel.js | 281 + .../themes/grav/app/forms/fields/parents.js | 270 + .../grav/app/forms/fields/permissions.js | 46 + .../themes/grav/app/forms/fields/range.js | 10 + .../themes/grav/app/forms/fields/selectize.js | 66 + .../grav/app/forms/fields/selectunique.js | 160 + .../themes/grav/app/forms/fields/text.js | 13 + .../admin/themes/grav/app/forms/form.js | 139 + .../admin/themes/grav/app/forms/index.js | 16 + .../admin/themes/grav/app/forms/state.js | 150 + user/plugins/admin/themes/grav/app/main.js | 72 + .../admin/themes/grav/app/media/index.js | 226 + .../admin/themes/grav/app/pages/filter.js | 148 + .../admin/themes/grav/app/pages/index.js | 53 + .../admin/themes/grav/app/pages/page/add.js | 67 + .../themes/grav/app/pages/page/delete.js | 15 + .../grav/app/pages/page/disable-buttons.js | 5 + .../admin/themes/grav/app/pages/page/index.js | 47 + .../admin/themes/grav/app/pages/page/media.js | 205 + .../admin/themes/grav/app/pages/page/move.js | 63 + .../themes/grav/app/pages/page/multilang.js | 22 + .../admin/themes/grav/app/pages/page/unset.js | 18 + .../admin/themes/grav/app/pages/tree.js | 125 + .../admin/themes/grav/app/plugins/index.js | 92 + .../admin/themes/grav/app/themes/index.js | 46 + .../admin/themes/grav/app/tools/index.js | 1 + .../admin/themes/grav/app/tools/logs.js | 14 + .../grav/app/updates/channel-switcher.js | 24 + .../admin/themes/grav/app/updates/check.js | 26 + .../admin/themes/grav/app/updates/feed.js | 68 + .../admin/themes/grav/app/updates/index.js | 196 + .../themes/grav/app/updates/notifications.js | 169 + .../admin/themes/grav/app/updates/update.js | 22 + .../admin/themes/grav/app/utils/2fa.js | 32 + .../grav/app/utils/bootstrap-collapse.js | 210 + .../app/utils/bootstrap-datetimepicker.js | 2632 + .../grav/app/utils/bootstrap-dropdown.js | 169 + .../grav/app/utils/bootstrap-transition.js | 52 + .../admin/themes/grav/app/utils/changelog.js | 25 + .../admin/themes/grav/app/utils/colors.js | 152 + .../admin/themes/grav/app/utils/cookies.js | 164 + .../admin/themes/grav/app/utils/cron-ui.js | 864 + .../admin/themes/grav/app/utils/finderjs.js | 325 + .../themes/grav/app/utils/formatbytes.js | 11 + .../admin/themes/grav/app/utils/gpm.js | 57 + .../themes/grav/app/utils/jquery-utils.js | 50 + .../admin/themes/grav/app/utils/keepalive.js | 34 + .../admin/themes/grav/app/utils/offline.js | 21 + .../admin/themes/grav/app/utils/packages.js | 470 + .../admin/themes/grav/app/utils/remodal.js | 820 + .../admin/themes/grav/app/utils/request.js | 38 + .../admin/themes/grav/app/utils/response.js | 101 + .../admin/themes/grav/app/utils/scrollbar.js | 1 + .../grav/app/utils/selectize-option-click.js | 42 + .../grav/app/utils/selectize-required-fix.js | 28 + .../admin/themes/grav/app/utils/sidebar.js | 181 + .../admin/themes/grav/app/utils/storage.js | 41 + .../themes/grav/app/utils/tabs-memory.js | 29 + .../admin/themes/grav/app/utils/toastr.js | 6 + .../themes/grav/app/whitelabel/compile.js | 25 + .../admin/themes/grav/app/whitelabel/index.js | 93 + .../themes/grav/app/whitelabel/presets.js | 169 + .../admin/themes/grav/css-compiled/fonts.css | 3 + .../themes/grav/css-compiled/fonts.css.map | 1 + .../themes/grav/css-compiled/nucleus.css | 625 + .../themes/grav/css-compiled/nucleus.css.map | 93 + .../themes/grav/css-compiled/paper.css.map | 1 + .../admin/themes/grav/css-compiled/preset.css | 1517 + .../themes/grav/css-compiled/preset.css.map | 18 + .../themes/grav/css-compiled/simple-fonts.css | 13 + .../grav/css-compiled/simple-fonts.css.map | 10 + .../themes/grav/css-compiled/template.css | 5607 + .../themes/grav/css-compiled/template.css.map | 129 + .../admin/themes/grav/css/chartist.min.css | 1 + .../themes/grav/css/codemirror/codemirror.css | 332 + .../grav/css/codemirror/themes/3024-day.css | 41 + .../grav/css/codemirror/themes/3024-night.css | 39 + .../grav/css/codemirror/themes/abcdef.css | 32 + .../css/codemirror/themes/ambiance-mobile.css | 5 + .../grav/css/codemirror/themes/ambiance.css | 74 + .../grav/css/codemirror/themes/ayu-dark.css | 42 + .../grav/css/codemirror/themes/ayu-mirage.css | 43 + .../css/codemirror/themes/base16-dark.css | 38 + .../css/codemirror/themes/base16-light.css | 38 + .../grav/css/codemirror/themes/bespin.css | 34 + .../grav/css/codemirror/themes/blackboard.css | 32 + .../grav/css/codemirror/themes/cobalt.css | 25 + .../grav/css/codemirror/themes/colorforth.css | 33 + .../grav/css/codemirror/themes/darcula.css | 53 + .../grav/css/codemirror/themes/dracula.css | 40 + .../css/codemirror/themes/duotone-dark.css | 35 + .../css/codemirror/themes/duotone-light.css | 36 + .../grav/css/codemirror/themes/eclipse.css | 23 + .../grav/css/codemirror/themes/elegant.css | 13 + .../css/codemirror/themes/erlang-dark.css | 34 + .../css/codemirror/themes/gruvbox-dark.css | 37 + .../grav/css/codemirror/themes/hopscotch.css | 34 + .../grav/css/codemirror/themes/icecoder.css | 43 + .../grav/css/codemirror/themes/idea.css | 42 + .../grav/css/codemirror/themes/isotope.css | 34 + .../css/codemirror/themes/lesser-dark.css | 47 + .../grav/css/codemirror/themes/liquibyte.css | 95 + .../grav/css/codemirror/themes/lucario.css | 37 + .../css/codemirror/themes/material-darker.css | 135 + .../css/codemirror/themes/material-ocean.css | 135 + .../codemirror/themes/material-palenight.css | 135 + .../grav/css/codemirror/themes/material.css | 135 + .../themes/grav/css/codemirror/themes/mbo.css | 37 + .../grav/css/codemirror/themes/mdn-like.css | 46 + .../grav/css/codemirror/themes/midnight.css | 39 + .../grav/css/codemirror/themes/monokai.css | 41 + .../grav/css/codemirror/themes/moxer.css | 143 + .../grav/css/codemirror/themes/neat.css | 12 + .../themes/grav/css/codemirror/themes/neo.css | 43 + .../grav/css/codemirror/themes/night.css | 27 + .../grav/css/codemirror/themes/nord.css | 42 + .../css/codemirror/themes/oceanic-next.css | 44 + .../css/codemirror/themes/panda-syntax.css | 85 + .../grav/css/codemirror/themes/paper.css | 143 + .../css/codemirror/themes/paraiso-dark.css | 38 + .../css/codemirror/themes/paraiso-light.css | 38 + .../css/codemirror/themes/pastel-on-dark.css | 52 + .../grav/css/codemirror/themes/railscasts.css | 34 + .../grav/css/codemirror/themes/rubyblue.css | 25 + .../grav/css/codemirror/themes/seti.css | 44 + .../grav/css/codemirror/themes/shadowfox.css | 52 + .../grav/css/codemirror/themes/solarized.css | 168 + .../grav/css/codemirror/themes/ssms.css | 16 + .../grav/css/codemirror/themes/the-matrix.css | 30 + .../themes/tomorrow-night-bright.css | 35 + .../themes/tomorrow-night-eighties.css | 38 + .../grav/css/codemirror/themes/ttcn.css | 64 + .../grav/css/codemirror/themes/twilight.css | 32 + .../css/codemirror/themes/vibrant-ink.css | 34 + .../grav/css/codemirror/themes/xq-dark.css | 53 + .../grav/css/codemirror/themes/xq-light.css | 43 + .../grav/css/codemirror/themes/yeti.css | 44 + .../grav/css/codemirror/themes/yonce.css | 59 + .../grav/css/codemirror/themes/zenburn.css | 37 + .../themes/grav/css/featherlight.min.css | 8 + .../themes/grav/css/fork-awesome.min.css | 12 + .../admin/themes/grav/css/hint.base.min.css | 5 + .../admin/themes/grav/css/iconpicker.css | 222 + .../admin/themes/grav/css/nucleus-ie10.css | 24 + .../admin/themes/grav/css/nucleus-ie9.css | 62 + user/plugins/admin/themes/grav/css/prism.css | 121 + .../themes/grav/css/pure-0.5.0/grids-min.css | 15 + user/plugins/admin/themes/grav/css/rtl.css | 1240 + .../admin/themes/grav/css/selectize.min.css | 1 + .../themes/grav/fonts/forkawesome-webfont.eot | Bin 0 -> 186490 bytes .../themes/grav/fonts/forkawesome-webfont.svg | 2810 + .../themes/grav/fonts/forkawesome-webfont.ttf | Bin 0 -> 186300 bytes .../grav/fonts/forkawesome-webfont.woff | Bin 0 -> 113404 bytes .../grav/fonts/forkawesome-webfont.woff2 | Bin 0 -> 90420 bytes .../rockettheme-apps/rockettheme-apps.eot | Bin 0 -> 4838 bytes .../rockettheme-apps/rockettheme-apps.svg | 16 + .../rockettheme-apps/rockettheme-apps.ttf | Bin 0 -> 4644 bytes .../rockettheme-apps/rockettheme-apps.woff | Bin 0 -> 3348 bytes .../admin/themes/grav/images/favicon.png | Bin 0 -> 13203 bytes .../admin/themes/grav/images/grav-regular.svg | 1 + .../admin/themes/grav/images/grav-small.svg | 1 + .../plugins/admin/themes/grav/images/logo.svg | 1 + .../admin/themes/grav/images/transparent.png | Bin 0 -> 5123 bytes .../plugins/admin/themes/grav/js/admin.min.js | 15055 +++ .../themes/grav/js/form-attr.polyfill.js | 80 + .../admin/themes/grav/js/vendor.min.js | 93005 ++++++++++++++++ .../admin/themes/grav/package-lock.json | 9097 ++ user/plugins/admin/themes/grav/package.json | 68 + .../scss/configuration/nucleus/_base.scss | 14 + .../configuration/nucleus/_breakpoints.scss | 16 + .../scss/configuration/nucleus/_core.scss | 2 + .../scss/configuration/nucleus/_layout.scss | 8 + .../grav/scss/configuration/nucleus/_nav.scss | 3 + .../configuration/nucleus/_typography.scss | 14 + .../scss/configuration/template/_base.scss | 5 + .../scss/configuration/template/_colors.scss | 15 + .../configuration/template/_variables.scss | 22 + .../admin/themes/grav/scss/nucleus.scss | 27 + .../admin/themes/grav/scss/nucleus/_core.scss | 217 + .../admin/themes/grav/scss/nucleus/_flex.scss | 195 + .../themes/grav/scss/nucleus/_forms.scss | 62 + .../themes/grav/scss/nucleus/_typography.scss | 89 + .../grav/scss/nucleus/functions/_base.scss | 2 + .../scss/nucleus/functions/_direction.scss | 37 + .../grav/scss/nucleus/functions/_range.scss | 13 + .../grav/scss/nucleus/mixins/_base.scss | 2 + .../scss/nucleus/mixins/_breakpoints.scss | 44 + .../grav/scss/nucleus/mixins/_utilities.scss | 30 + .../scss/nucleus/particles/_align-text.scss | 46 + .../scss/nucleus/particles/_visibility.scss | 0 .../admin/themes/grav/scss/preset.scss | 2364 + .../admin/themes/grav/scss/simple-fonts.scss | 13 + .../admin/themes/grav/scss/template.scss | 109 + .../themes/grav/scss/template/_admin.scss | 1431 + .../themes/grav/scss/template/_buttons.scss | 131 + .../themes/grav/scss/template/_changelog.scss | 117 + .../themes/grav/scss/template/_colors.scss | 93 + .../themes/grav/scss/template/_core.scss | 51 + .../themes/grav/scss/template/_custom.scss | 2 + .../themes/grav/scss/template/_dropzone.scss | 285 + .../themes/grav/scss/template/_editor.scss | 315 + .../themes/grav/scss/template/_errors.scss | 17 + .../grav/scss/template/_extensions.scss | 58 + .../themes/grav/scss/template/_fonts.scss | 40 + .../themes/grav/scss/template/_forms.scss | 974 + .../admin/themes/grav/scss/template/_gpm.scss | 207 + .../themes/grav/scss/template/_header.scss | 3 + .../scss/template/_horizontal-scroll.scss | 54 + .../themes/grav/scss/template/_jqcron.scss | 70 + .../themes/grav/scss/template/_login.scss | 191 + .../themes/grav/scss/template/_media.scss | 331 + .../grav/scss/template/_nav-toggle.scss | 71 + .../grav/scss/template/_notifications.scss | 116 + .../themes/grav/scss/template/_pages.scss | 856 + .../themes/grav/scss/template/_phpinfo.scss | 35 + .../themes/grav/scss/template/_remodal.scss | 329 + .../grav/scss/template/_scrollbars.scss | 210 + .../themes/grav/scss/template/_tables.scss | 190 + .../themes/grav/scss/template/_tabs.scss | 125 + .../themes/grav/scss/template/_toastr.scss | 183 + .../themes/grav/scss/template/_tools.scss | 97 + .../grav/scss/template/_typography.scss | 166 + .../themes/grav/scss/template/_utilities.scss | 9 + .../grav/scss/template/_whitelabel.scss | 268 + .../grav/scss/template/modules/_base.scss | 5 + .../template/modules/_bourbon_essentials.scss | 120 + .../grav/scss/template/modules/_buttons.scss | 91 + .../scss/template/modules/_colorpicker.scss | 253 + .../template/modules/_datetimepicker.scss | 466 + .../grav/scss/template/modules/_finderjs.scss | 161 + .../scss/template/modules/_input-range.scss | 87 + .../scss/template/modules/_scrollbars.scss | 18 + .../scss/template/modules/_toggle-switch.scss | 20 + .../bourbon/_bourbon-deprecated-upcoming.scss | 8 + .../grav/scss/vendor/bourbon/_bourbon.scss | 77 + .../scss/vendor/bourbon/addons/_button.scss | 374 + .../scss/vendor/bourbon/addons/_clearfix.scss | 23 + .../bourbon/addons/_directional-values.scss | 111 + .../scss/vendor/bourbon/addons/_ellipsis.scss | 7 + .../vendor/bourbon/addons/_font-family.scss | 5 + .../vendor/bourbon/addons/_hide-text.scss | 10 + .../bourbon/addons/_html5-input-types.scss | 86 + .../scss/vendor/bourbon/addons/_position.scss | 32 + .../scss/vendor/bourbon/addons/_prefixer.scss | 45 + .../grav/scss/vendor/bourbon/addons/_rem.scss | 33 + .../vendor/bourbon/addons/_retina-image.scss | 31 + .../scss/vendor/bourbon/addons/_size.scss | 16 + .../bourbon/addons/_timing-functions.scss | 32 + .../scss/vendor/bourbon/addons/_triangle.scss | 83 + .../vendor/bourbon/addons/_word-wrap.scss | 8 + .../scss/vendor/bourbon/css3/_animation.scss | 52 + .../scss/vendor/bourbon/css3/_appearance.scss | 3 + .../bourbon/css3/_backface-visibility.scss | 6 + .../bourbon/css3/_background-image.scss | 42 + .../scss/vendor/bourbon/css3/_background.scss | 55 + .../vendor/bourbon/css3/_border-image.scss | 59 + .../vendor/bourbon/css3/_border-radius.scss | 22 + .../scss/vendor/bourbon/css3/_box-sizing.scss | 4 + .../grav/scss/vendor/bourbon/css3/_calc.scss | 4 + .../scss/vendor/bourbon/css3/_columns.scss | 47 + .../scss/vendor/bourbon/css3/_filter.scss | 5 + .../scss/vendor/bourbon/css3/_flex-box.scss | 321 + .../scss/vendor/bourbon/css3/_font-face.scss | 23 + .../bourbon/css3/_font-feature-settings.scss | 10 + .../bourbon/css3/_hidpi-media-query.scss | 10 + .../scss/vendor/bourbon/css3/_hyphens.scss | 4 + .../vendor/bourbon/css3/_image-rendering.scss | 14 + .../scss/vendor/bourbon/css3/_keyframes.scss | 35 + .../vendor/bourbon/css3/_linear-gradient.scss | 38 + .../vendor/bourbon/css3/_perspective.scss | 8 + .../vendor/bourbon/css3/_placeholder.scss | 8 + .../vendor/bourbon/css3/_radial-gradient.scss | 39 + .../scss/vendor/bourbon/css3/_transform.scss | 15 + .../scss/vendor/bourbon/css3/_transition.scss | 77 + .../vendor/bourbon/css3/_user-select.scss | 3 + .../vendor/bourbon/functions/_assign.scss | 11 + .../bourbon/functions/_color-lightness.scss | 13 + .../vendor/bourbon/functions/_flex-grid.scss | 39 + .../bourbon/functions/_golden-ratio.scss | 3 + .../vendor/bourbon/functions/_grid-width.scss | 13 + .../bourbon/functions/_modular-scale.scss | 66 + .../vendor/bourbon/functions/_px-to-em.scss | 13 + .../vendor/bourbon/functions/_px-to-rem.scss | 15 + .../bourbon/functions/_strip-units.scss | 5 + .../vendor/bourbon/functions/_tint-shade.scss | 9 + .../functions/_transition-property-name.scss | 22 + .../vendor/bourbon/functions/_unpack.scss | 17 + .../bourbon/helpers/_convert-units.scss | 15 + .../helpers/_gradient-positions-parser.scss | 13 + .../scss/vendor/bourbon/helpers/_is-num.scss | 8 + .../bourbon/helpers/_linear-angle-parser.scss | 25 + .../helpers/_linear-gradient-parser.scss | 41 + .../helpers/_linear-positions-parser.scss | 61 + .../helpers/_linear-side-corner-parser.scss | 31 + .../bourbon/helpers/_radial-arg-parser.scss | 69 + .../helpers/_radial-gradient-parser.scss | 50 + .../helpers/_radial-positions-parser.scss | 18 + .../bourbon/helpers/_render-gradients.scss | 26 + .../bourbon/helpers/_shape-size-stripper.scss | 10 + .../vendor/bourbon/helpers/_str-to-num.scss | 50 + .../vendor/bourbon/settings/_prefixer.scss | 6 + .../vendor/bourbon/settings/_px-to-em.scss | 1 + .../vendor/color-schemer/_color-schemer.scss | 31 + .../color-schemer/color-schemer/_cmyk.scss | 14 + .../color-schemer/_color-adjustments.scss | 30 + .../color-schemer/_color-schemer.scss | 208 + .../color-schemer/_colorblind.scss | 29 + .../color-schemer/_comparison.scss | 15 + .../color-schemer/_equalize.scss | 5 + .../color-schemer/_harmonize.scss | 59 + .../color-schemer/_interpolation.scss | 34 + .../color-schemer/color-schemer/_mix.scss | 40 + .../color-schemer/color-schemer/_mixins.scss | 29 + .../color-schemer/color-schemer/_ryb.scss | 76 + .../color-schemer/_tint-shade.scss | 9 + .../themes/grav/templates/changelog.html.twig | 21 + .../themes/grav/templates/config.html.twig | 82 + .../themes/grav/templates/dashboard.html.twig | 73 + .../themes/grav/templates/default.json.twig | 5 + .../themes/grav/templates/denied.html.twig | 18 + .../grav/templates/email/base.html.twig | 194 + .../themes/grav/templates/empty.html.twig | 0 .../themes/grav/templates/error.html.twig | 20 + .../themes/grav/templates/error.json.twig | 1 + .../themes/grav/templates/forgot.html.twig | 14 + .../grav/templates/forms/field.html.twig | 130 + .../fields/acl_picker/acl_picker.html.twig | 134 + .../forms/fields/array/array.html.twig | 96 + .../backupshistory/backupshistory.html.twig | 32 + .../fields/blueprint/blueprint.html.twig | 12 + .../fields/codemirror/codemirror.html.twig | 10 + .../fields/colorpicker/colorpicker.html.twig | 39 + .../colorscheme.color.html.twig | 40 + .../fields/colorscheme/colorscheme.html.twig | 15 + .../forms/fields/column/column.html.twig | 8 + .../forms/fields/columns/columns.html.twig | 8 + .../forms/fields/cron/cron.html.twig | 7 + .../fields/cronstatus/cronstatus.html.twig | 71 + .../fields/dateformat/dateformat.html.twig | 24 + .../forms/fields/datetime/datetime.html.twig | 27 + .../forms/fields/editor/editor.html.twig | 62 + .../forms/fields/fieldset/fieldset.html.twig | 81 + .../forms/fields/file/file.html.twig | 118 + .../fields/filepicker/filepicker.html.twig | 27 + .../fields/folder-slug/folder-slug.html.twig | 8 + .../fields/frontmatter/frontmatter.html.twig | 10 + .../fields/iconpicker/iconpicker.html.twig | 46 + .../forms/fields/iconpicker/icons.yaml | 6570 ++ .../forms/fields/list/list.html.twig | 177 + .../forms/fields/markdown/markdown.html.twig | 1 + .../fields/mediapicker/mediapicker.html.twig | 20 + .../fields/multilevel/multilevel.html.twig | 121 + .../forms/fields/order/order.html.twig | 64 + .../fields/pagemedia/pagemedia.html.twig | 58 + .../pagemediaselect/pagemediaselect.html.twig | 1 + .../forms/fields/pages/pages.html.twig | 52 + .../forms/fields/parents/parents.html.twig | 58 + .../fields/permissions/permissions.html.twig | 161 + .../forms/fields/range/range.html.twig | 28 + .../forms/fields/section/section.html.twig | 24 + .../fields/selectize/selectize.html.twig | 19 + .../selectunique/selectunique.html.twig | 43 + .../forms/fields/taxonomy/taxonomy.html.twig | 38 + .../themepreview/themepreview.html.twig | 106 + .../fields/themeselect/themeselect.html.twig | 31 + .../forms/fields/userinfo/userinfo.html.twig | 19 + .../forms/fields/widgets/widgets.html.twig | 61 + .../templates/forms/fields/xss/xss.html.twig | 6 + .../themes/grav/templates/installer.html.twig | 13 + .../themes/grav/templates/login.html.twig | 11 + .../templates/media-list-content.html.twig | 19 + .../themes/grav/templates/pages.html.twig | 469 + .../partials/backups-button.html.twig | 21 + .../templates/partials/base-root.html.twig | 156 + .../grav/templates/partials/base.html.twig | 1 + .../partials/blueprints-copy.html.twig | 19 + .../partials/blueprints-new-folder.html.twig | 18 + .../partials/blueprints-new.html.twig | 19 + .../partials/blueprints-raw.html.twig | 22 + .../templates/partials/blueprints.html.twig | 34 + .../partials/dashboard-feed.html.twig | 14 + .../partials/dashboard-maintenance.html.twig | 33 + .../dashboard-notifications.html.twig | 13 + .../partials/dashboard-pages.html.twig | 20 + .../partials/dashboard-problems.html.twig | 11 + .../partials/dashboard-statistics.html.twig | 24 + .../templates/partials/feed-block.html.twig | 3 + .../grav/templates/partials/footer.html.twig | 5 + .../grav/templates/partials/header.html.twig | 11 + .../partials/javascript-config.html.twig | 43 + .../templates/partials/javascripts.html.twig | 11 + .../templates/partials/list-sort.html.twig | 12 + .../templates/partials/login-form.html.twig | 25 + .../templates/partials/login-logo.html.twig | 14 + .../templates/partials/login-logout.html.twig | 15 + .../templates/partials/login-twofa.html.twig | 20 + .../grav/templates/partials/login.html.twig | 33 + .../grav/templates/partials/logo.html.twig | 20 + .../partials/media-list-wrapper.html.twig | 32 + .../media-list-wrapper__list.html.twig | 23 + ...edia-list-wrapper__list__filters.html.twig | 28 + .../media-list-wrapper__sidebar.html.twig | 36 + .../templates/partials/messages.html.twig | 14 + .../partials/modal-add-package.html.twig | 89 + .../partials/modal-changelog.html.twig | 1 + .../partials/modal-changes-detected.html.twig | 13 + .../modal-reinstall-package.html.twig | 38 + .../partials/modal-remove-package.html.twig | 44 + .../partials/modal-switch-theme.html.twig | 16 + .../partials/modal-update-packages.html.twig | 89 + .../partials/nav-quick-tray.html.twig | 47 + .../templates/partials/nav-toggle.html.twig | 3 + .../partials/nav-user-avatar.html.twig | 2 + .../partials/nav-user-details.html.twig | 21 + .../grav/templates/partials/nav.html.twig | 47 + .../notification-dashboard-block.html.twig | 6 + .../notification-feed-block.html.twig | 3 + .../notification-plugins-block.html.twig | 6 + .../notification-themes-block.html.twig | 6 + .../partials/notification-top-block.html.twig | 6 + .../partials/page-children.html.twig | 11 + .../templates/partials/page-legend.html.twig | 11 + .../templates/partials/page-move.html.twig | 21 + .../templates/partials/plugin-data.html.twig | 74 + .../partials/plugins-details.html.twig | 76 + .../templates/partials/plugins-list.html.twig | 75 + .../templates/partials/register.html.twig | 24 + .../partials/release-toggle.html.twig | 11 + .../partials/spinning-wheel.html.twig | 3 + .../templates/partials/stylesheets.html.twig | 16 + .../grav/templates/partials/theme.html.twig | 13 + .../partials/themes-details.html.twig | 161 + .../templates/partials/themes-list.html.twig | 80 + .../partials/tools-backups-titlebar.html.twig | 7 + .../partials/tools-backups.html.twig | 81 + .../partials/tools-direct-install.html.twig | 29 + .../templates/partials/tools-logs.html.twig | 80 + .../partials/tools-reports.html.twig | 15 + .../tools-scheduler-titlebar.html.twig | 6 + .../partials/tools-scheduler.html.twig | 36 + .../partials/userinfo-avatar-credit.html.twig | 1 + .../partials/userinfo-avatar.html.twig | 5 + .../themes/grav/templates/plugins.html.twig | 94 + .../templates/plugins/admin-buttons.html.twig | 3 + .../themes/grav/templates/preview.html.twig | 24 + .../themes/grav/templates/register.html.twig | 24 + .../grav/templates/reports/security.html.twig | 20 + .../templates/reports/yamllinter.html.twig | 31 + .../themes/grav/templates/reset.html.twig | 14 + .../grav/templates/statistics.html.twig | 15 + .../themes/grav/templates/themes.html.twig | 87 + .../themes/grav/templates/tools.html.twig | 50 + .../grav/templates/unauthorized.html.twig | 1 + .../themes/grav/templates/user.html.twig | 63 + user/plugins/admin/themes/grav/theme.yaml | 0 user/plugins/admin/themes/grav/watch.sh | 27 + .../plugins/admin/themes/grav/webpack.conf.js | 54 + user/plugins/admin/themes/grav/yarn.lock | 3558 + .../plugins/admin/twig/AdminTwigExtension.php | 7 + user/plugins/admin/vendor/autoload.php | 7 + user/plugins/admin/vendor/bin/picofeed | 1 + user/plugins/admin/vendor/bin/pscss | 1 + .../admin/vendor/composer/ClassLoader.php | 445 + .../vendor/composer/InstalledVersions.php | 283 + user/plugins/admin/vendor/composer/LICENSE | 21 + .../vendor/composer/autoload_classmap.php | 11 + .../admin/vendor/composer/autoload_files.php | 10 + .../vendor/composer/autoload_namespaces.php | 10 + .../admin/vendor/composer/autoload_psr4.php | 13 + .../admin/vendor/composer/autoload_real.php | 75 + .../admin/vendor/composer/autoload_static.php | 73 + .../admin/vendor/composer/installed.json | 272 + .../admin/vendor/composer/installed.php | 88 + .../admin/vendor/composer/platform_check.php | 26 + .../workflows/continuous-integration.yml | 33 + .../workflows/release-on-milestone-closed.yml | 71 + .../vendor/laminas/laminas-xml/COPYRIGHT.md | 1 + .../vendor/laminas/laminas-xml/LICENSE.md | 26 + .../vendor/laminas/laminas-xml/README.md | 46 + .../vendor/laminas/laminas-xml/composer.json | 55 + .../vendor/laminas/laminas-xml/composer.lock | 2295 + .../src/Exception/ExceptionInterface.php | 13 + .../Exception/InvalidArgumentException.php | 16 + .../src/Exception/RuntimeException.php | 16 + .../laminas/laminas-xml/src/Security.php | 416 + .../.github/FUNDING.yml | 1 + .../workflows/continuous-integration.yml | 32 + .../workflows/release-on-milestone-closed.yml | 61 + .../.laminas-ci.json | 5 + .../laminas-zendframework-bridge/COPYRIGHT.md | 1 + .../laminas-zendframework-bridge/LICENSE.md | 26 + .../laminas-zendframework-bridge/README.md | 30 + .../composer.json | 61 + .../composer.lock | 3841 + .../config/replacements.php | 372 + .../psalm-baseline.xml | 363 + .../psalm.xml.dist | 33 + .../src/Autoloader.php | 166 + .../src/ConfigPostProcessor.php | 426 + .../src/Module.php | 48 + .../src/Replacements.php | 40 + .../src/RewriteRules.php | 73 + .../src/autoload.php | 3 + .../plugins/admin/vendor/p3k/picofeed/LICENSE | 21 + .../vendor/p3k/picofeed/lib/PicoFeed/Base.php | 38 + .../picofeed/lib/PicoFeed/Client/Client.php | 712 + .../lib/PicoFeed/Client/ClientException.php | 14 + .../p3k/picofeed/lib/PicoFeed/Client/Curl.php | 412 + .../PicoFeed/Client/ForbiddenException.php | 10 + .../lib/PicoFeed/Client/HttpHeaders.php | 79 + .../Client/InvalidCertificateException.php | 12 + .../PicoFeed/Client/InvalidUrlException.php | 12 + .../PicoFeed/Client/MaxRedirectException.php | 12 + .../lib/PicoFeed/Client/MaxSizeException.php | 12 + .../picofeed/lib/PicoFeed/Client/Stream.php | 205 + .../lib/PicoFeed/Client/TimeoutException.php | 12 + .../PicoFeed/Client/UnauthorizedException.php | 10 + .../p3k/picofeed/lib/PicoFeed/Client/Url.php | 283 + .../picofeed/lib/PicoFeed/Config/Config.php | 102 + .../lib/PicoFeed/Encoding/Encoding.php | 33 + .../lib/PicoFeed/Filter/Attribute.php | 700 + .../picofeed/lib/PicoFeed/Filter/Filter.php | 144 + .../p3k/picofeed/lib/PicoFeed/Filter/Html.php | 243 + .../p3k/picofeed/lib/PicoFeed/Filter/Tag.php | 208 + .../Generator/ContentGeneratorInterface.php | 23 + .../Generator/FileContentGenerator.php | 36 + .../Generator/YoutubeContentGenerator.php | 67 + .../picofeed/lib/PicoFeed/Logging/Logger.php | 114 + .../p3k/picofeed/lib/PicoFeed/Parser/Atom.php | 395 + .../lib/PicoFeed/Parser/DateParser.php | 128 + .../p3k/picofeed/lib/PicoFeed/Parser/Feed.php | 315 + .../p3k/picofeed/lib/PicoFeed/Parser/Item.php | 562 + .../PicoFeed/Parser/MalformedXmlException.php | 13 + .../picofeed/lib/PicoFeed/Parser/Parser.php | 397 + .../lib/PicoFeed/Parser/ParserException.php | 15 + .../lib/PicoFeed/Parser/ParserInterface.php | 191 + .../picofeed/lib/PicoFeed/Parser/Rss10.php | 318 + .../picofeed/lib/PicoFeed/Parser/Rss20.php | 330 + .../picofeed/lib/PicoFeed/Parser/Rss91.php | 13 + .../picofeed/lib/PicoFeed/Parser/Rss92.php | 13 + .../PicoFeed/Parser/XmlEntityException.php | 13 + .../lib/PicoFeed/Parser/XmlParser.php | 249 + .../lib/PicoFeed/PicoFeedException.php | 14 + .../Processor/ContentFilterProcessor.php | 39 + .../Processor/ContentGeneratorProcessor.php | 49 + .../PicoFeed/Processor/ItemPostProcessor.php | 106 + .../Processor/ItemProcessorInterface.php | 25 + .../PicoFeed/Processor/ScraperProcessor.php | 96 + .../picofeed/lib/PicoFeed/Reader/Favicon.php | 186 + .../picofeed/lib/PicoFeed/Reader/Reader.php | 189 + .../lib/PicoFeed/Reader/ReaderException.php | 14 + .../Reader/SubscriptionNotFoundException.php | 12 + .../Reader/UnsupportedFeedFormatException.php | 12 + .../lib/PicoFeed/Rules/.blog.lemonde.fr.php | 14 + .../lib/PicoFeed/Rules/.blogs.nytimes.com.php | 15 + .../picofeed/lib/PicoFeed/Rules/.igen.fr.php | 13 + .../lib/PicoFeed/Rules/.nytimes.com.php | 11 + .../lib/PicoFeed/Rules/.over-blog.com.php | 11 + .../lib/PicoFeed/Rules/.phoronix.com.php | 12 + .../lib/PicoFeed/Rules/.slate.com.php | 20 + .../lib/PicoFeed/Rules/.theguardian.com.php | 14 + .../lib/PicoFeed/Rules/.wikipedia.org.php | 29 + .../lib/PicoFeed/Rules/.wired.com.php | 44 + .../picofeed/lib/PicoFeed/Rules/.wsj.com.php | 15 + .../picofeed/lib/PicoFeed/Rules/01net.com.php | 18 + .../p3k/picofeed/lib/PicoFeed/Rules/24.hu.php | 16 + .../picofeed/lib/PicoFeed/Rules/444.hu.php | 19 + .../picofeed/lib/PicoFeed/Rules/888.hu.php | 16 + .../lib/PicoFeed/Rules/abstrusegoose.com.php | 8 + .../lib/PicoFeed/Rules/achgut.com.php | 16 + .../PicoFeed/Rules/adventuregamers.com.php | 23 + .../lib/PicoFeed/Rules/alainonline.net.php | 14 + .../lib/PicoFeed/Rules/aljazeera.com.php | 25 + .../lib/PicoFeed/Rules/allafrica.com.php | 20 + .../PicoFeed/Rules/allgemeine-zeitung.de.php | 23 + .../PicoFeed/Rules/amazingsuperpowers.com.php | 8 + .../lib/PicoFeed/Rules/anythingcomic.com.php | 13 + .../picofeed/lib/PicoFeed/Rules/ap.org.php | 13 + .../lib/PicoFeed/Rules/areadvd.de.php | 10 + .../lib/PicoFeed/Rules/arstechnica.com.php | 25 + .../picofeed/lib/PicoFeed/Rules/atv.hu.php | 18 + .../lib/PicoFeed/Rules/awkwardzombie.com.php | 10 + .../lib/PicoFeed/Rules/backchannel.com.php | 18 + .../lib/PicoFeed/Rules/bangkokpost.com.php | 19 + .../lib/PicoFeed/Rules/bauerwilli.com.php | 17 + .../picofeed/lib/PicoFeed/Rules/bgr.com.php | 15 + .../lib/PicoFeed/Rules/bigfootjustice.com.php | 8 + .../lib/PicoFeed/Rules/bigpicture.ru.php | 31 + .../lib/PicoFeed/Rules/bizjournals.com.php | 12 + .../lib/PicoFeed/Rules/biztimes.com.php | 22 + .../PicoFeed/Rules/bleepingcomputer.com.php | 15 + .../lib/PicoFeed/Rules/blog.fefe.de.php | 13 + .../lib/PicoFeed/Rules/blog.mapillary.com.php | 11 + .../lib/PicoFeed/Rules/brewers.mlb.com.php | 22 + .../PicoFeed/Rules/buenosairesherald.com.php | 17 + .../lib/PicoFeed/Rules/bunicomic.com.php | 13 + .../lib/PicoFeed/Rules/buttersafe.com.php | 13 + .../lib/PicoFeed/Rules/cad-comic.com.php | 12 + .../Rules/chaoslife.findchaos.com.php | 10 + .../lib/PicoFeed/Rules/chinafile.com.php | 18 + .../picofeed/lib/PicoFeed/Rules/cicero.de.php | 17 + .../lib/PicoFeed/Rules/cliquerefresh.com.php | 10 + .../picofeed/lib/PicoFeed/Rules/cnet.com.php | 37 + .../lib/PicoFeed/Rules/coinwelt.de.php | 14 + .../lib/PicoFeed/Rules/consomac.fr.php | 13 + .../lib/PicoFeed/Rules/cowbirdsinlove.com.php | 8 + .../picofeed/lib/PicoFeed/Rules/crash.net.php | 18 + .../lib/PicoFeed/Rules/csmonitor.com.php | 19 + .../lib/PicoFeed/Rules/dailyjs.com.php | 19 + .../lib/PicoFeed/Rules/dailyreporter.com.php | 15 + .../lib/PicoFeed/Rules/dailytech.com.php | 13 + .../lib/PicoFeed/Rules/degroupnews.com.php | 14 + .../lib/PicoFeed/Rules/derstandard.at.php | 14 + .../lib/PicoFeed/Rules/dilbert.com.php | 11 + .../PicoFeed/Rules/discovermagazine.com.php | 26 + .../lib/PicoFeed/Rules/distrowatch.com.php | 13 + .../lib/PicoFeed/Rules/dozodomo.com.php | 15 + .../PicoFeed/Rules/drawingboardcomic.com.php | 15 + .../picofeed/lib/PicoFeed/Rules/e-w-e.ru.php | 22 + .../lib/PicoFeed/Rules/economist.com.php | 25 + .../Rules/encyclopedie.naheulbeuk.com.php | 13 + .../lib/PicoFeed/Rules/endlessorigami.com.php | 8 + .../lib/PicoFeed/Rules/engadget.com.php | 10 + .../PicoFeed/Rules/escapistmagazine.com.php | 45 + .../lib/PicoFeed/Rules/espn.go.com.php | 11 + .../lib/PicoFeed/Rules/exocomics.com.php | 15 + .../lib/PicoFeed/Rules/explosm.net.php | 13 + .../Rules/extrafabulouscomics.com.php | 8 + .../lib/PicoFeed/Rules/factroom.ru.php | 27 + .../lib/PicoFeed/Rules/fastcodesign.com.php | 13 + .../lib/PicoFeed/Rules/fastcoexist.com.php | 13 + .../lib/PicoFeed/Rules/fastcompany.com.php | 13 + .../lib/PicoFeed/Rules/ffworld.com.php | 13 + .../lib/PicoFeed/Rules/foreignpolicy.com.php | 21 + .../lib/PicoFeed/Rules/fossbytes.com.php | 18 + .../lib/PicoFeed/Rules/fototelegraf.ru.php | 19 + .../PicoFeed/Rules/fowllanguagecomics.com.php | 10 + .../lib/PicoFeed/Rules/gamechannel.hu.php | 15 + .../lib/PicoFeed/Rules/gamestar.hu.php | 17 + .../picofeed/lib/PicoFeed/Rules/geek.com.php | 17 + .../lib/PicoFeed/Rules/geektimes.ru.php | 12 + .../PicoFeed/Rules/gerbilwithajetpack.com.php | 12 + .../lib/PicoFeed/Rules/giantitp.com.php | 12 + .../lib/PicoFeed/Rules/github.com.php | 14 + .../lib/PicoFeed/Rules/gocomics.com.php | 12 + .../picofeed/lib/PicoFeed/Rules/golem.de.php | 21 + .../lib/PicoFeed/Rules/gondola.hu.php | 22 + .../lib/PicoFeed/Rules/gorabbit.ru.php | 19 + .../lib/PicoFeed/Rules/habrahabr.ru.php | 12 + .../lib/PicoFeed/Rules/happletea.com.php | 18 + .../lib/PicoFeed/Rules/hardware.fr.php | 11 + .../picofeed/lib/PicoFeed/Rules/heise.de.php | 79 + .../lib/PicoFeed/Rules/hirek.prim.hu.php | 14 + .../lib/PicoFeed/Rules/hotshowlife.com.php | 23 + .../lib/PicoFeed/Rules/huffingtonpost.com.php | 13 + .../picofeed/lib/PicoFeed/Rules/hvg.hu.php | 17 + .../picofeed/lib/PicoFeed/Rules/idokep.hu.php | 18 + .../lib/PicoFeed/Rules/imogenquest.net.php | 8 + .../picofeed/lib/PicoFeed/Rules/index.hu.php | 29 + .../lib/PicoFeed/Rules/indiehaven.com.php | 11 + .../lib/PicoFeed/Rules/inforadio.hu.php | 25 + .../picofeed/lib/PicoFeed/Rules/ing.dk.php | 12 + .../lib/PicoFeed/Rules/invisiblebread.com.php | 8 + .../lib/PicoFeed/Rules/ir.amd.com.php | 10 + .../lib/PicoFeed/Rules/japantimes.co.jp.php | 21 + .../lib/PicoFeed/Rules/japantoday.com.php | 15 + .../lib/PicoFeed/Rules/journaldugeek.com.php | 11 + .../lib/PicoFeed/Rules/jsonline.com.php | 37 + .../lib/PicoFeed/Rules/justcoolidea.ru.php | 19 + .../picofeed/lib/PicoFeed/Rules/kanpai.fr.php | 13 + .../PicoFeed/Rules/karriere.jobfinder.dk.php | 12 + .../lib/PicoFeed/Rules/kisalfold.hu.php | 17 + .../lib/PicoFeed/Rules/kiszamolo.hu.php | 14 + .../picofeed/lib/PicoFeed/Rules/kodi.tv.php | 11 + .../lib/PicoFeed/Rules/koreaherald.com.php | 11 + .../lib/PicoFeed/Rules/koreatimes.php | 14 + .../PicoFeed/Rules/lastplacecomics.com.php | 8 + .../lib/PicoFeed/Rules/legorafi.fr.php | 22 + .../lib/PicoFeed/Rules/lejapon.fr.php | 17 + .../lib/PicoFeed/Rules/lesjoiesducode.fr.php | 13 + .../picofeed/lib/PicoFeed/Rules/lfg.co.php | 12 + .../lib/PicoFeed/Rules/lifehacker.com.php | 18 + .../lib/PicoFeed/Rules/lifehacker.ru.php | 22 + .../lib/PicoFeed/Rules/linux-magazin.de.php | 16 + .../picofeed/lib/PicoFeed/Rules/linux.org.php | 14 + .../lib/PicoFeed/Rules/linux.org.ru.php | 13 + .../lib/PicoFeed/Rules/linuxinsider.com.php | 20 + .../p3k/picofeed/lib/PicoFeed/Rules/lists.php | 13 + .../lib/PicoFeed/Rules/loadingartist.com.php | 8 + .../lib/PicoFeed/Rules/loldwell.com.php | 10 + .../lib/PicoFeed/Rules/lukesurl.com.php | 15 + .../picofeed/lib/PicoFeed/Rules/macg.co.php | 13 + .../lib/PicoFeed/Rules/maclife.de.php | 11 + .../lib/PicoFeed/Rules/magyarkurir.hu.php | 21 + .../picofeed/lib/PicoFeed/Rules/marc.info.php | 13 + .../PicoFeed/Rules/marriedtothesea.com.php | 12 + .../lib/PicoFeed/Rules/marycagle.com.php | 13 + .../Rules/maximumble.thebookofbiff.com.php | 10 + .../lib/PicoFeed/Rules/medium.com.php | 19 + .../lib/PicoFeed/Rules/mercworks.net.php | 17 + .../lib/PicoFeed/Rules/metronieuws.nl.php | 10 + .../lib/PicoFeed/Rules/milwaukeenns.php | 14 + .../picofeed/lib/PicoFeed/Rules/mno.hu.php | 14 + .../Rules/mokepon.smackjeeves.com.php | 10 + .../lib/PicoFeed/Rules/monandroid.com.php | 13 + .../lib/PicoFeed/Rules/monwindows.com.php | 13 + .../lib/PicoFeed/Rules/moya-planeta.ru.php | 21 + .../lib/PicoFeed/Rules/mrlovenstein.com.php | 9 + .../lib/PicoFeed/Rules/muckrock.com.php | 20 + .../PicoFeed/Rules/mynorthshorenow.com.php | 27 + .../lib/PicoFeed/Rules/nakedCapitalism.php | 11 + .../picofeed/lib/PicoFeed/Rules/nasa.gov.php | 14 + .../lib/PicoFeed/Rules/nat-geo.ru.php | 11 + .../PicoFeed/Rules/nationaljournal.com.php | 15 + .../lib/PicoFeed/Rules/nature.com.php | 13 + .../picofeed/lib/PicoFeed/Rules/nba.com.php | 15 + .../lib/PicoFeed/Rules/nedroid.com.php | 8 + .../lib/PicoFeed/Rules/networkworld.com.php | 20 + .../lib/PicoFeed/Rules/neustadt-ticker.de.php | 15 + .../lib/PicoFeed/Rules/nextinpact.com.php | 18 + .../lib/PicoFeed/Rules/niceteethcomic.com.php | 10 + .../lib/PicoFeed/Rules/nichtlustig.de.php | 8 + .../picofeed/lib/PicoFeed/Rules/nlcafe.hu.php | 18 + .../lib/PicoFeed/Rules/novo-argumente.com.php | 16 + .../picofeed/lib/PicoFeed/Rules/oglaf.com.php | 19 + .../picofeed/lib/PicoFeed/Rules/onhax.net.php | 15 + .../lib/PicoFeed/Rules/onlinekosten.de.php | 13 + .../lib/PicoFeed/Rules/onmilwaukee.php | 24 + .../lib/PicoFeed/Rules/openculture.com.php | 11 + .../lib/PicoFeed/Rules/opennet.ru.php | 13 + .../PicoFeed/Rules/openrightsgroup.org.php | 20 + .../lib/PicoFeed/Rules/opensource.com.php | 22 + .../lib/PicoFeed/Rules/optipess.com.php | 8 + .../picofeed/lib/PicoFeed/Rules/origo.hu.php | 14 + .../lib/PicoFeed/Rules/osnews.com.php | 11 + .../lib/PicoFeed/Rules/pastebin.com.php | 13 + .../lib/PicoFeed/Rules/pcgameshardware.de.php | 16 + .../lib/PicoFeed/Rules/peebleslab.com.php | 9 + .../lib/PicoFeed/Rules/penny-arcade.com.php | 21 + .../lib/PicoFeed/Rules/pixelbeat.org.php | 12 + .../lib/PicoFeed/Rules/plus.google.com.php | 11 + .../lib/PicoFeed/Rules/popstrip.com.php | 8 + .../lib/PicoFeed/Rules/portfolio.hu.php | 15 + .../lib/PicoFeed/Rules/pro-linux.de.php | 16 + .../PicoFeed/Rules/publicpolicyforum.org.php | 15 + .../picofeed/lib/PicoFeed/Rules/publy.ru.php | 24 + .../lib/PicoFeed/Rules/putaindecode.fr.php | 16 + .../lib/PicoFeed/Rules/recode.net.php | 20 + .../PicoFeed/Rules/retractionwatch.com.php | 18 + .../PicoFeed/Rules/rockpapershotgun.com.php | 11 + .../PicoFeed/Rules/rue89.nouvelobs.com.php | 13 + .../lib/PicoFeed/Rules/rugbyrama.fr.php | 20 + .../lib/PicoFeed/Rules/salonkolumnisten.com | 17 + .../lib/PicoFeed/Rules/satwcomic.com.php | 13 + .../PicoFeed/Rules/science-skeptical.de.php | 14 + .../lib/PicoFeed/Rules/scrumalliance.org.php | 12 + .../lib/PicoFeed/Rules/securityfocus.com.php | 17 + .../PicoFeed/Rules/sentfromthemoon.com.php | 18 + .../lib/PicoFeed/Rules/sitepoint.com.php | 13 + .../lib/PicoFeed/Rules/slashdot.org.php | 11 + .../PicoFeed/Rules/smallhousebliss.com.php | 19 + .../lib/PicoFeed/Rules/smarthomewelt.de.php | 10 + .../PicoFeed/Rules/smashingmagazine.com.php | 10 + .../lib/PicoFeed/Rules/smbc-comics.com.php | 14 + .../lib/PicoFeed/Rules/snopes.com.php | 22 + .../lib/PicoFeed/Rules/soundandvision.com.php | 21 + .../lib/PicoFeed/Rules/spiegel.de.php | 14 + .../lib/PicoFeed/Rules/stereophile.com.php | 11 + .../lib/PicoFeed/Rules/stupidfox.net.php | 13 + .../lib/PicoFeed/Rules/subtraction.com.php | 15 + .../p3k/picofeed/lib/PicoFeed/Rules/sz.de.php | 10 + .../lib/PicoFeed/Rules/takprosto.cc.php | 21 + .../lib/PicoFeed/Rules/techcrunch.com.php | 15 + .../PicoFeed/Rules/the-ebook-reader.com.php | 15 + .../lib/PicoFeed/Rules/theatlantic.com.php | 23 + .../lib/PicoFeed/Rules/theawkwardyeti.com.php | 12 + .../lib/PicoFeed/Rules/thecodinglove.com.php | 10 + .../PicoFeed/Rules/thedoghousediaries.com.php | 18 + .../lib/PicoFeed/Rules/thegamercat.com.php | 10 + .../lib/PicoFeed/Rules/thehindu.com.php | 19 + .../lib/PicoFeed/Rules/thelocal.se.php | 17 + .../lib/PicoFeed/Rules/themerepublic.net.php | 10 + .../lib/PicoFeed/Rules/themoscowtimes.com.php | 18 + .../lib/PicoFeed/Rules/thenewslens.com.php | 21 + .../lib/PicoFeed/Rules/theodd1sout.com.php | 8 + .../lib/PicoFeed/Rules/theonion.com.php | 12 + .../lib/PicoFeed/Rules/theregister.co.uk.php | 18 + .../lib/PicoFeed/Rules/thestandard.com.hk.php | 22 + .../lib/PicoFeed/Rules/theverge.com.php | 16 + .../lib/PicoFeed/Rules/threepanelsoul.com.php | 11 + .../lib/PicoFeed/Rules/tichyseinblick.de.php | 22 + .../Rules/timesofindia.indiatimes.com.php | 14 + .../lib/PicoFeed/Rules/totalcar.hu.php | 18 + .../lib/PicoFeed/Rules/tozsdeforum.hu.php | 15 + .../lib/PicoFeed/Rules/travel-dealz.de.php | 15 + .../lib/PicoFeed/Rules/travelo.hu.php | 17 + .../lib/PicoFeed/Rules/treehugger.com.php | 14 + .../lib/PicoFeed/Rules/treelobsters.com.php | 8 + .../lib/PicoFeed/Rules/tutorialzine.com.php | 20 + .../lib/PicoFeed/Rules/twogag.com.php | 8 + .../PicoFeed/Rules/twokinds.keenspot.com.php | 10 + .../lib/PicoFeed/Rules/undeadly.org.php | 14 + .../picofeed/lib/PicoFeed/Rules/upi.com.php | 15 + .../lib/PicoFeed/Rules/usatoday.com.php | 27 + .../lib/PicoFeed/Rules/version2.dk.php | 12 + .../picofeed/lib/PicoFeed/Rules/vezess.hu.php | 20 + .../lib/PicoFeed/Rules/vgcats.com.php | 15 + .../picofeed/lib/PicoFeed/Rules/vuxml.org.php | 17 + .../PicoFeed/Rules/wausaudailyherald.com.php | 27 + .../picofeed/lib/PicoFeed/Rules/welt.de.php | 20 + .../lib/PicoFeed/Rules/westfalen-blatt.de.php | 16 + .../lib/PicoFeed/Rules/www.bbc.co.uk.php | 33 + .../lib/PicoFeed/Rules/www.bdgest.com.php | 15 + .../lib/PicoFeed/Rules/www.bgr.in.php | 23 + .../PicoFeed/Rules/www.businessweek.com.php | 15 + .../lib/PicoFeed/Rules/www.cnn.com.php | 24 + .../lib/PicoFeed/Rules/www.developpez.com.php | 21 + .../lib/PicoFeed/Rules/www.egscomics.com.php | 12 + .../Rules/www.fakingnews.firstpost.com.php | 17 + .../lib/PicoFeed/Rules/www.forbes.com.php | 20 + .../PicoFeed/Rules/www.franceculture.fr.php | 14 + .../Rules/www.futura-sciences.com.php | 19 + .../PicoFeed/Rules/www.geekculture.com.php | 13 + .../lib/PicoFeed/Rules/www.howtogeek.com.php | 14 + .../lib/PicoFeed/Rules/www.lepoint.fr.php | 18 + .../PicoFeed/Rules/www.lesnumeriques.com.php | 25 + .../lib/PicoFeed/Rules/www.mac4ever.com.php | 13 + .../lib/PicoFeed/Rules/www.makeuseof.com.php | 18 + .../Rules/www.monsieur-le-chien.fr.php | 11 + .../lib/PicoFeed/Rules/www.npr.org.php | 28 + .../lib/PicoFeed/Rules/www.numerama.com.php | 15 + .../lib/PicoFeed/Rules/www.oneindia.com.php | 14 + .../Rules/www.pseudo-sciences.org.php | 16 + .../lib/PicoFeed/Rules/www.sciencemag.org.php | 16 + .../lib/PicoFeed/Rules/www.slate.fr.php | 19 + .../PicoFeed/Rules/www.universfreebox.com.php | 15 + .../lib/PicoFeed/Rules/www.zeit.de.php | 41 + .../picofeed/lib/PicoFeed/Rules/xkcd.com.php | 8 + .../lib/PicoFeed/Rules/ymatuhin.ru.php | 21 + .../lib/PicoFeed/Rules/zarojel.hu.php | 19 + .../picofeed/lib/PicoFeed/Rules/zdnet.com.php | 23 + .../picofeed/lib/PicoFeed/Rules/zoom.hu.php | 17 + .../lib/PicoFeed/Scraper/CandidateParser.php | 281 + .../lib/PicoFeed/Scraper/ParserInterface.php | 20 + .../lib/PicoFeed/Scraper/RuleLoader.php | 104 + .../lib/PicoFeed/Scraper/RuleParser.php | 102 + .../picofeed/lib/PicoFeed/Scraper/Scraper.php | 282 + .../PicoFeed/Serialization/Subscription.php | 175 + .../Serialization/SubscriptionList.php | 75 + .../Serialization/SubscriptionListBuilder.php | 204 + .../Serialization/SubscriptionListParser.php | 100 + .../Serialization/SubscriptionParser.php | 142 + .../PicoFeed/Syndication/AtomFeedBuilder.php | 65 + .../lib/PicoFeed/Syndication/AtomHelper.php | 139 + .../PicoFeed/Syndication/AtomItemBuilder.php | 63 + .../lib/PicoFeed/Syndication/FeedBuilder.php | 185 + .../lib/PicoFeed/Syndication/ItemBuilder.php | 209 + .../PicoFeed/Syndication/Rss20FeedBuilder.php | 76 + .../lib/PicoFeed/Syndication/Rss20Helper.php | 115 + .../PicoFeed/Syndication/Rss20ItemBuilder.php | 67 + .../admin/vendor/p3k/picofeed/picofeed | 135 + .../admin/vendor/scssphp/scssphp/LICENSE.md | 20 + .../admin/vendor/scssphp/scssphp/README.md | 71 + .../admin/vendor/scssphp/scssphp/bin/pscss | 244 + .../vendor/scssphp/scssphp/composer.json | 108 + .../vendor/scssphp/scssphp/phpcs.xml.dist | 12 + .../admin/vendor/scssphp/scssphp/scss.inc.php | 21 + .../vendor/scssphp/scssphp/src/Base/Range.php | 57 + .../vendor/scssphp/scssphp/src/Block.php | 73 + .../vendor/scssphp/scssphp/src/Cache.php | 272 + .../vendor/scssphp/scssphp/src/Colors.php | 247 + .../scssphp/scssphp/src/CompilationResult.php | 69 + .../vendor/scssphp/scssphp/src/Compiler.php | 10087 ++ .../scssphp/src/Compiler/CachedResult.php | 77 + .../scssphp/src/Compiler/Environment.php | 48 + .../src/Exception/CompilerException.php | 24 + .../scssphp/src/Exception/ParserException.php | 50 + .../scssphp/src/Exception/RangeException.php | 24 + .../scssphp/src/Exception/SassException.php | 7 + .../src/Exception/SassScriptException.php | 32 + .../scssphp/src/Exception/ServerException.php | 26 + .../vendor/scssphp/scssphp/src/Formatter.php | 364 + .../scssphp/scssphp/src/Formatter/Compact.php | 52 + .../scssphp/src/Formatter/Compressed.php | 85 + .../scssphp/src/Formatter/Crunched.php | 87 + .../scssphp/scssphp/src/Formatter/Debug.php | 127 + .../scssphp/src/Formatter/Expanded.php | 70 + .../scssphp/scssphp/src/Formatter/Nested.php | 236 + .../scssphp/src/Formatter/OutputBlock.php | 68 + .../scssphp/src/Logger/LoggerInterface.php | 48 + .../scssphp/src/Logger/QuietLogger.php | 27 + .../scssphp/src/Logger/StreamLogger.php | 60 + .../admin/vendor/scssphp/scssphp/src/Node.php | 43 + .../scssphp/scssphp/src/Node/Number.php | 804 + .../scssphp/scssphp/src/OutputStyle.php | 9 + .../vendor/scssphp/scssphp/src/Parser.php | 4128 + .../scssphp/scssphp/src/SourceMap/Base64.php | 187 + .../scssphp/src/SourceMap/Base64VLQ.php | 151 + .../src/SourceMap/SourceMapGenerator.php | 381 + .../admin/vendor/scssphp/scssphp/src/Type.php | 76 + .../admin/vendor/scssphp/scssphp/src/Util.php | 184 + .../vendor/scssphp/scssphp/src/Util/Path.php | 77 + .../scssphp/scssphp/src/ValueConverter.php | 95 + .../vendor/scssphp/scssphp/src/Version.php | 23 + .../admin/vendor/scssphp/scssphp/src/Warn.php | 84 + user/plugins/advanced-pagecache/CHANGELOG.md | 64 + user/plugins/advanced-pagecache/LICENSE | 22 + user/plugins/advanced-pagecache/README.md | 56 + .../advanced-pagecache/advanced-pagecache.php | 150 + .../advanced-pagecache.yaml | 13 + .../advanced-pagecache/blueprints.yaml | 106 + user/plugins/aura/.gitignore | 2 + user/plugins/aura/CHANGELOG.md | 77 + user/plugins/aura/LICENSE | 21 + user/plugins/aura/README.md | 42 + .../aura/assets/demo-composition-min.png | Bin 0 -> 135930 bytes user/plugins/aura/aura.php | 187 + user/plugins/aura/aura.yaml | 1 + user/plugins/aura/blueprints.yaml | 142 + user/plugins/aura/blueprints/default.yaml | 61 + user/plugins/aura/classes/aura.php | 535 + user/plugins/aura/classes/image.php | 12 + user/plugins/aura/classes/organization.php | 11 + user/plugins/aura/classes/person.php | 12 + user/plugins/aura/classes/webpage.php | 16 + user/plugins/aura/classes/website.php | 9 + user/plugins/aura/composer.json | 37 + user/plugins/automagic-images/CHANGELOG.md | 42 + user/plugins/automagic-images/LICENSE | 21 + user/plugins/automagic-images/README.md | 97 + user/plugins/automagic-images/adapters/gd.php | 113 + .../automagic-images/adapters/imagick.php | 82 + .../automagic-images/adapters/interface.php | 15 + .../automagic-images/automagic-images.php | 228 + .../automagic-images/automagic-images.yaml | 27 + user/plugins/automagic-images/blueprints.yaml | 76 + user/plugins/automagic-images/composer.json | 5 + user/plugins/automagic-images/composer.lock | 698 + user/plugins/automagic-images/composer.phar | Bin 0 -> 2216568 bytes user/plugins/automagic-images/languages.yaml | 28 + .../automagic-images/vendor/autoload.php | 7 + .../vendor/composer/ClassLoader.php | 479 + .../vendor/composer/InstalledVersions.php | 398 + .../automagic-images/vendor/composer/LICENSE | 21 + .../vendor/composer/autoload_classmap.php | 10 + .../vendor/composer/autoload_files.php | 13 + .../vendor/composer/autoload_namespaces.php | 10 + .../vendor/composer/autoload_psr4.php | 18 + .../vendor/composer/autoload_real.php | 75 + .../vendor/composer/autoload_static.php | 103 + .../vendor/composer/installed.json | 718 + .../vendor/composer/installed.php | 137 + .../vendor/composer/platform_check.php | 26 + .../vendor/guzzlehttp/guzzle/CHANGELOG.md | 1462 + .../vendor/guzzlehttp/guzzle/LICENSE | 19 + .../vendor/guzzlehttp/guzzle/README.md | 79 + .../vendor/guzzlehttp/guzzle/UPGRADING.md | 1253 + .../vendor/guzzlehttp/guzzle/composer.json | 73 + .../guzzlehttp/guzzle/src/BodySummarizer.php | 28 + .../guzzle/src/BodySummarizerInterface.php | 13 + .../vendor/guzzlehttp/guzzle/src/Client.php | 474 + .../guzzlehttp/guzzle/src/ClientInterface.php | 84 + .../guzzlehttp/guzzle/src/ClientTrait.php | 241 + .../guzzle/src/Cookie/CookieJar.php | 313 + .../guzzle/src/Cookie/CookieJarInterface.php | 79 + .../guzzle/src/Cookie/FileCookieJar.php | 101 + .../guzzle/src/Cookie/SessionCookieJar.php | 77 + .../guzzle/src/Cookie/SetCookie.php | 410 + .../src/Exception/BadResponseException.php | 39 + .../guzzle/src/Exception/ClientException.php | 10 + .../guzzle/src/Exception/ConnectException.php | 56 + .../guzzle/src/Exception/GuzzleException.php | 9 + .../Exception/InvalidArgumentException.php | 7 + .../guzzle/src/Exception/RequestException.php | 166 + .../guzzle/src/Exception/ServerException.php | 10 + .../Exception/TooManyRedirectsException.php | 7 + .../src/Exception/TransferException.php | 7 + .../guzzle/src/Handler/CurlFactory.php | 592 + .../src/Handler/CurlFactoryInterface.php | 25 + .../guzzle/src/Handler/CurlHandler.php | 49 + .../guzzle/src/Handler/CurlMultiHandler.php | 253 + .../guzzle/src/Handler/EasyHandle.php | 112 + .../guzzle/src/Handler/HeaderProcessor.php | 42 + .../guzzle/src/Handler/MockHandler.php | 211 + .../guzzlehttp/guzzle/src/Handler/Proxy.php | 51 + .../guzzle/src/Handler/StreamHandler.php | 581 + .../guzzlehttp/guzzle/src/HandlerStack.php | 271 + .../guzzle/src/MessageFormatter.php | 198 + .../guzzle/src/MessageFormatterInterface.php | 18 + .../guzzlehttp/guzzle/src/Middleware.php | 260 + .../vendor/guzzlehttp/guzzle/src/Pool.php | 125 + .../guzzle/src/PrepareBodyMiddleware.php | 104 + .../guzzle/src/RedirectMiddleware.php | 216 + .../guzzlehttp/guzzle/src/RequestOptions.php | 264 + .../guzzlehttp/guzzle/src/RetryMiddleware.php | 116 + .../guzzlehttp/guzzle/src/TransferStats.php | 133 + .../vendor/guzzlehttp/guzzle/src/Utils.php | 382 + .../guzzlehttp/guzzle/src/functions.php | 167 + .../guzzle/src/functions_include.php | 6 + .../vendor-bin/php-cs-fixer/composer.json | 9 + .../guzzle/vendor-bin/phpstan/composer.json | 10 + .../guzzle/vendor-bin/psalm/composer.json | 9 + .../vendor/guzzlehttp/promises/CHANGELOG.md | 84 + .../vendor/guzzlehttp/promises/LICENSE | 19 + .../vendor/guzzlehttp/promises/Makefile | 13 + .../vendor/guzzlehttp/promises/README.md | 532 + .../vendor/guzzlehttp/promises/composer.json | 39 + .../promises/src/AggregateException.php | 17 + .../promises/src/CancellationException.php | 10 + .../guzzlehttp/promises/src/Coroutine.php | 169 + .../vendor/guzzlehttp/promises/src/Create.php | 84 + .../vendor/guzzlehttp/promises/src/Each.php | 90 + .../guzzlehttp/promises/src/EachPromise.php | 254 + .../promises/src/FulfilledPromise.php | 84 + .../vendor/guzzlehttp/promises/src/Is.php | 46 + .../guzzlehttp/promises/src/Promise.php | 278 + .../promises/src/PromiseInterface.php | 97 + .../promises/src/PromisorInterface.php | 16 + .../promises/src/RejectedPromise.php | 91 + .../promises/src/RejectionException.php | 48 + .../guzzlehttp/promises/src/TaskQueue.php | 67 + .../promises/src/TaskQueueInterface.php | 24 + .../vendor/guzzlehttp/promises/src/Utils.php | 274 + .../guzzlehttp/promises/src/functions.php | 363 + .../promises/src/functions_include.php | 6 + .../vendor/guzzlehttp/psr7/CHANGELOG.md | 294 + .../vendor/guzzlehttp/psr7/LICENSE | 19 + .../vendor/guzzlehttp/psr7/README.md | 809 + .../vendor/guzzlehttp/psr7/composer.json | 49 + .../guzzlehttp/psr7/src/AppendStream.php | 246 + .../guzzlehttp/psr7/src/BufferStream.php | 142 + .../guzzlehttp/psr7/src/CachingStream.php | 141 + .../guzzlehttp/psr7/src/DroppingStream.php | 45 + .../vendor/guzzlehttp/psr7/src/FnStream.php | 163 + .../vendor/guzzlehttp/psr7/src/Header.php | 71 + .../guzzlehttp/psr7/src/InflateStream.php | 56 + .../guzzlehttp/psr7/src/LazyOpenStream.php | 42 + .../guzzlehttp/psr7/src/LimitStream.php | 157 + .../vendor/guzzlehttp/psr7/src/Message.php | 252 + .../guzzlehttp/psr7/src/MessageTrait.php | 214 + .../vendor/guzzlehttp/psr7/src/MimeType.php | 140 + .../guzzlehttp/psr7/src/MultipartStream.php | 158 + .../guzzlehttp/psr7/src/NoSeekStream.php | 25 + .../vendor/guzzlehttp/psr7/src/PumpStream.php | 170 + .../vendor/guzzlehttp/psr7/src/Query.php | 113 + .../vendor/guzzlehttp/psr7/src/Request.php | 152 + .../vendor/guzzlehttp/psr7/src/Response.php | 155 + .../vendor/guzzlehttp/psr7/src/Rfc7230.php | 19 + .../guzzlehttp/psr7/src/ServerRequest.php | 379 + .../vendor/guzzlehttp/psr7/src/Stream.php | 270 + .../psr7/src/StreamDecoratorTrait.php | 152 + .../guzzlehttp/psr7/src/StreamWrapper.php | 165 + .../guzzlehttp/psr7/src/UploadedFile.php | 328 + .../vendor/guzzlehttp/psr7/src/Uri.php | 810 + .../guzzlehttp/psr7/src/UriNormalizer.php | 219 + .../guzzlehttp/psr7/src/UriResolver.php | 222 + .../vendor/guzzlehttp/psr7/src/Utils.php | 428 + .../vendor/guzzlehttp/psr7/src/functions.php | 422 + .../guzzlehttp/psr7/src/functions_include.php | 6 + .../vendor/myclabs/php-enum/LICENSE | 18 + .../vendor/myclabs/php-enum/README.md | 138 + .../vendor/myclabs/php-enum/SECURITY.md | 11 + .../vendor/myclabs/php-enum/composer.json | 33 + .../vendor/myclabs/php-enum/psalm.xml | 35 + .../vendor/myclabs/php-enum/src/Enum.php | 305 + .../php-enum/src/PHPUnit/Comparator.php | 54 + .../paquettg/php-html-parser/SECURITY.md | 11 + .../paquettg/php-html-parser/composer.json | 38 + .../src/PHPHtmlParser/Content.php | 257 + .../Contracts/Dom/CleanerInterface.php | 16 + .../Contracts/Dom/ParserInterface.php | 33 + .../PHPHtmlParser/Contracts/DomInterface.php | 23 + .../Contracts/Selector/ParserInterface.php | 12 + .../Contracts/Selector/SeekerInterface.php | 17 + .../Contracts/Selector/SelectorInterface.php | 31 + .../Selector/ParsedSelectorCollectionDTO.php | 41 + .../DTO/Selector/ParsedSelectorDTO.php | 41 + .../PHPHtmlParser/DTO/Selector/RuleDTO.php | 100 + .../PHPHtmlParser/DTO/Tag/AttributeDTO.php | 60 + .../src/PHPHtmlParser/DTO/TagDTO.php | 74 + .../Discovery/CleanerDiscovery.php | 25 + .../Discovery/DomParserDiscovery.php | 25 + .../Discovery/SeekerDiscovery.php | 25 + .../Discovery/SelectorParserDiscovery.php | 25 + .../php-html-parser/src/PHPHtmlParser/Dom.php | 251 + .../src/PHPHtmlParser/Dom/Cleaner.php | 130 + .../PHPHtmlParser/Dom/Node/AbstractNode.php | 495 + .../src/PHPHtmlParser/Dom/Node/ArrayNode.php | 45 + .../src/PHPHtmlParser/Dom/Node/Collection.php | 156 + .../src/PHPHtmlParser/Dom/Node/HtmlNode.php | 244 + .../src/PHPHtmlParser/Dom/Node/InnerNode.php | 442 + .../src/PHPHtmlParser/Dom/Node/LeafNode.php | 21 + .../src/PHPHtmlParser/Dom/Node/TextNode.php | 155 + .../src/PHPHtmlParser/Dom/Parser.php | 348 + .../src/PHPHtmlParser/Dom/RootAccessTrait.php | 100 + .../src/PHPHtmlParser/Dom/Tag.php | 365 + .../src/PHPHtmlParser/Enum/StringToken.php | 23 + .../Exceptions/ChildNotFoundException.php | 14 + .../Exceptions/CircularException.php | 14 + .../Exceptions/ContentLengthException.php | 14 + .../Exceptions/CurlException.php | 14 + .../Exceptions/EmptyCollectionException.php | 14 + .../Exceptions/LogicalException.php | 14 + .../Exceptions/NotLoadedException.php | 14 + .../Exceptions/ParentNotFoundException.php | 14 + .../Exceptions/StrictException.php | 14 + .../Tag/AttributeNotFoundException.php | 12 + .../Exceptions/UnknownChildTypeException.php | 14 + .../Exceptions/UnknownOptionException.php | 14 + .../src/PHPHtmlParser/Finder.php | 64 + .../src/PHPHtmlParser/Options.php | 367 + .../src/PHPHtmlParser/Selector/Parser.php | 116 + .../src/PHPHtmlParser/Selector/Seeker.php | 316 + .../src/PHPHtmlParser/Selector/Selector.php | 105 + .../src/PHPHtmlParser/StaticDom.php | 114 + .../paquettg/string-encode/CONTRIBUTING.md | 21 + .../vendor/paquettg/string-encode/README.md | 28 + .../paquettg/string-encode/composer.json | 28 + .../vendor/paquettg/string-encode/phpunit.php | 28 + .../vendor/paquettg/string-encode/phpunit.xml | 26 + .../string-encode/src/stringEncode/Encode.php | 121 + .../src/stringEncode/Exception.php | 4 + .../string-encode/tests/EncodeTest.php | 30 + .../vendor/php-http/httplug/CHANGELOG.md | 129 + .../vendor/php-http/httplug/LICENSE | 20 + .../vendor/php-http/httplug/README.md | 63 + .../vendor/php-http/httplug/composer.json | 45 + .../vendor/php-http/httplug/puli.json | 12 + .../vendor/php-http/httplug/src/Exception.php | 14 + .../httplug/src/Exception/HttpException.php | 65 + .../src/Exception/NetworkException.php | 28 + .../src/Exception/RequestAwareTrait.php | 26 + .../src/Exception/RequestException.php | 29 + .../src/Exception/TransferException.php | 14 + .../php-http/httplug/src/HttpAsyncClient.php | 25 + .../php-http/httplug/src/HttpClient.php | 15 + .../src/Promise/HttpFulfilledPromise.php | 54 + .../src/Promise/HttpRejectedPromise.php | 53 + .../vendor/php-http/promise/CHANGELOG.md | 48 + .../vendor/php-http/promise/LICENSE | 19 + .../vendor/php-http/promise/README.md | 48 + .../vendor/php-http/promise/composer.json | 38 + .../php-http/promise/src/FulfilledPromise.php | 58 + .../vendor/php-http/promise/src/Promise.php | 69 + .../php-http/promise/src/RejectedPromise.php | 58 + .../vendor/psr/http-client/CHANGELOG.md | 23 + .../vendor/psr/http-client/LICENSE | 19 + .../vendor/psr/http-client/README.md | 12 + .../vendor/psr/http-client/composer.json | 27 + .../src/ClientExceptionInterface.php | 10 + .../psr/http-client/src/ClientInterface.php | 20 + .../src/NetworkExceptionInterface.php | 24 + .../src/RequestExceptionInterface.php | 24 + .../vendor/psr/http-message/CHANGELOG.md | 36 + .../vendor/psr/http-message/LICENSE | 19 + .../vendor/psr/http-message/README.md | 13 + .../vendor/psr/http-message/composer.json | 26 + .../psr/http-message/src/MessageInterface.php | 187 + .../psr/http-message/src/RequestInterface.php | 129 + .../http-message/src/ResponseInterface.php | 68 + .../src/ServerRequestInterface.php | 261 + .../psr/http-message/src/StreamInterface.php | 158 + .../src/UploadedFileInterface.php | 123 + .../psr/http-message/src/UriInterface.php | 323 + .../vendor/ralouphie/getallheaders/LICENSE | 21 + .../vendor/ralouphie/getallheaders/README.md | 27 + .../ralouphie/getallheaders/composer.json | 26 + .../getallheaders/src/getallheaders.php | 46 + user/plugins/devtools/CHANGELOG.md | 197 + user/plugins/devtools/LICENSE | 21 + user/plugins/devtools/README.md | 79 + user/plugins/devtools/blueprints.yaml | 41 + .../devtools/classes/DevToolsCommand.php | 362 + .../devtools/cli/NewBlueprintCommand.php | 88 + .../plugins/devtools/cli/NewPluginCommand.php | 146 + user/plugins/devtools/cli/NewThemeCommand.php | 171 + .../blueprint/append/blueprint.yaml.twig | 21 + .../blueprint/newtab/blueprint.yaml.twig | 15 + .../components/plugin/blank/CHANGELOG.md.twig | 5 + .../components/plugin/blank/LICENSE.twig | 21 + .../components/plugin/blank/README.md.twig | 59 + .../plugin/blank/blueprints.yaml.twig | 37 + .../components/plugin/blank/classes/.gitkeep | 0 .../plugin/blank/composer.json.twig | 30 + .../plugin/blank/languages.yaml.twig | 4 + .../components/plugin/blank/plugin.php.twig | 59 + .../components/plugin/blank/plugin.yaml.twig | 2 + .../theme/inheritance/CHANGELOG.md.twig | 5 + .../components/theme/inheritance/LICENSE.twig | 21 + .../theme/inheritance/README.md.twig | 7 + .../theme/inheritance/blueprints.yaml.twig | 19 + .../components/theme/inheritance/css/.gitkeep | 0 .../components/theme/inheritance/js/.gitkeep | 0 .../theme/inheritance/screenshot.jpg | Bin 0 -> 15428 bytes .../theme/inheritance/templates/.gitkeep | 0 .../theme/inheritance/theme.php.twig | 9 + .../theme/inheritance/theme.yaml.twig | 10 + .../theme/inheritance/thumbnail.jpg | Bin 0 -> 3681 bytes .../theme/pure-blank/CHANGELOG.md.twig | 5 + .../components/theme/pure-blank/LICENSE.twig | 21 + .../theme/pure-blank/README.md.twig | 7 + .../theme/pure-blank/blueprints.yaml.twig | 33 + .../theme/pure-blank/css/custom.css | 175 + .../theme/pure-blank/fonts/.gitkeep | 0 .../theme/pure-blank/images/logo.png | Bin 0 -> 8714 bytes .../components/theme/pure-blank/js/.gitkeep | 0 .../theme/pure-blank/screenshot.jpg | Bin 0 -> 15428 bytes .../pure-blank/templates/default.html.twig | 5 + .../pure-blank/templates/error.html.twig | 8 + .../templates/partials/base.html.twig | 70 + .../templates/partials/navigation.html.twig | 47 + .../theme/pure-blank/theme.php.twig | 9 + .../theme/pure-blank/theme.yaml.twig | 3 + .../components/theme/pure-blank/thumbnail.jpg | Bin 0 -> 3681 bytes .../theme/tailwind/CHANGELOG.md.twig | 5 + .../components/theme/tailwind/LICENSE.twig | 21 + .../components/theme/tailwind/README.md.twig | 41 + .../theme/tailwind/blueprints.yaml.twig | 43 + .../components/theme/tailwind/css/site.css | 5 + .../components/theme/tailwind/fonts/.gitkeep | 0 .../components/theme/tailwind/images/logo.png | Bin 0 -> 8714 bytes .../components/theme/tailwind/js/.gitkeep | 0 .../theme/tailwind/languages/en.yaml.twig | 2 + .../theme/tailwind/package.json.twig | 30 + .../theme/tailwind/postcss.config.js | 11 + .../components/theme/tailwind/screenshot.jpg | Bin 0 -> 15428 bytes .../theme/tailwind/tailwind.config.js.twig | 70 + .../tailwind/templates/default.html.twig | 5 + .../theme/tailwind/templates/error.html.twig | 8 + .../templates/partials/base.html.twig | 67 + .../templates/partials/navigation.html.twig | 47 + .../components/theme/tailwind/theme.php.twig | 9 + .../components/theme/tailwind/theme.yaml.twig | 4 + .../components/theme/tailwind/thumbnail.jpg | Bin 0 -> 3681 bytes user/plugins/devtools/devtools.php | 13 + user/plugins/devtools/devtools.yaml | 2 + user/plugins/devtools/languages.yaml | 3 + user/plugins/email/CHANGELOG.md | 314 + user/plugins/email/LICENSE | 21 + user/plugins/email/README.md | 491 + user/plugins/email/blueprints.yaml | 251 + user/plugins/email/classes/Email.php | 560 + user/plugins/email/classes/Utils.php | 48 + .../email/cli/ClearQueueFailuresCommand.php | 62 + user/plugins/email/cli/FlushQueueCommand.php | 63 + user/plugins/email/cli/TestEmailCommand.php | 107 + user/plugins/email/composer.json | 33 + user/plugins/email/composer.lock | 651 + user/plugins/email/email.php | 181 + user/plugins/email/email.yaml | 23 + user/plugins/email/hebe.json | 15 + user/plugins/email/languages.yaml | 165 + .../email/templates/email/base.html.twig | 214 + user/plugins/email/vendor/autoload.php | 7 + .../email/vendor/composer/ClassLoader.php | 445 + .../vendor/composer/InstalledVersions.php | 291 + user/plugins/email/vendor/composer/LICENSE | 21 + .../vendor/composer/autoload_classmap.php | 12 + .../email/vendor/composer/autoload_files.php | 15 + .../vendor/composer/autoload_namespaces.php | 9 + .../email/vendor/composer/autoload_psr4.php | 18 + .../email/vendor/composer/autoload_real.php | 75 + .../email/vendor/composer/autoload_static.php | 96 + .../email/vendor/composer/installed.json | 660 + .../email/vendor/composer/installed.php | 96 + .../email/vendor/composer/platform_check.php | 26 + .../email/vendor/doctrine/lexer/LICENSE | 19 + .../email/vendor/doctrine/lexer/README.md | 9 + .../email/vendor/doctrine/lexer/composer.json | 41 + .../Doctrine/Common/Lexer/AbstractLexer.php | 328 + .../vendor/egulias/email-validator/LICENSE | 19 + .../egulias/email-validator/composer.json | 38 + .../email-validator/src/EmailLexer.php | 283 + .../email-validator/src/EmailParser.php | 137 + .../email-validator/src/EmailValidator.php | 67 + .../src/Exception/AtextAfterCFWS.php | 9 + .../src/Exception/CRLFAtTheEnd.php | 9 + .../email-validator/src/Exception/CRLFX2.php | 9 + .../email-validator/src/Exception/CRNoLF.php | 9 + .../src/Exception/CharNotAllowed.php | 9 + .../src/Exception/CommaInDomain.php | 9 + .../src/Exception/ConsecutiveAt.php | 9 + .../src/Exception/ConsecutiveDot.php | 9 + .../src/Exception/DomainAcceptsNoMail.php | 9 + .../src/Exception/DomainHyphened.php | 9 + .../src/Exception/DotAtEnd.php | 9 + .../src/Exception/DotAtStart.php | 9 + .../src/Exception/ExpectingAT.php | 9 + .../src/Exception/ExpectingATEXT.php | 9 + .../src/Exception/ExpectingCTEXT.php | 9 + .../src/Exception/ExpectingDTEXT.php | 9 + .../Exception/ExpectingDomainLiteralClose.php | 9 + .../src/Exception/ExpectingQPair.php | 9 + .../src/Exception/InvalidEmail.php | 14 + .../src/Exception/LocalOrReservedDomain.php | 9 + .../src/Exception/NoDNSRecord.php | 9 + .../src/Exception/NoDomainPart.php | 9 + .../src/Exception/NoLocalPart.php | 9 + .../src/Exception/UnclosedComment.php | 9 + .../src/Exception/UnclosedQuotedString.php | 9 + .../src/Exception/UnopenedComment.php | 9 + .../email-validator/src/Parser/DomainPart.php | 443 + .../email-validator/src/Parser/LocalPart.php | 145 + .../email-validator/src/Parser/Parser.php | 249 + .../src/Validation/DNSCheckValidation.php | 166 + .../src/Validation/EmailValidation.php | 34 + .../src/Validation/Error/RFCWarnings.php | 11 + .../src/Validation/Error/SpoofEmail.php | 11 + .../Exception/EmptyValidationList.php | 16 + .../src/Validation/MultipleErrors.php | 32 + .../Validation/MultipleValidationWithAnd.php | 124 + .../Validation/NoRFCWarningsValidation.php | 41 + .../src/Validation/RFCValidation.php | 49 + .../src/Validation/SpoofCheckValidation.php | 51 + .../src/Warning/AddressLiteral.php | 14 + .../src/Warning/CFWSNearAt.php | 13 + .../src/Warning/CFWSWithFWS.php | 13 + .../email-validator/src/Warning/Comment.php | 13 + .../src/Warning/DeprecatedComment.php | 13 + .../src/Warning/DomainLiteral.php | 14 + .../src/Warning/DomainTooLong.php | 14 + .../src/Warning/EmailTooLong.php | 15 + .../src/Warning/IPV6BadChar.php | 14 + .../src/Warning/IPV6ColonEnd.php | 14 + .../src/Warning/IPV6ColonStart.php | 14 + .../src/Warning/IPV6Deprecated.php | 14 + .../src/Warning/IPV6DoubleColon.php | 14 + .../src/Warning/IPV6GroupCount.php | 14 + .../src/Warning/IPV6MaxGroups.php | 14 + .../src/Warning/LabelTooLong.php | 14 + .../src/Warning/LocalTooLong.php | 15 + .../src/Warning/NoDNSMXRecord.php | 14 + .../src/Warning/ObsoleteDTEXT.php | 14 + .../src/Warning/QuotedPart.php | 17 + .../src/Warning/QuotedString.php | 17 + .../email-validator/src/Warning/TLD.php | 13 + .../email-validator/src/Warning/Warning.php | 47 + .../swiftmailer/swiftmailer/.gitattributes | 11 + .../swiftmailer/.github/ISSUE_TEMPLATE.md | 19 + .../.github/PULL_REQUEST_TEMPLATE.md | 14 + .../vendor/swiftmailer/swiftmailer/.gitignore | 9 + .../swiftmailer/swiftmailer/.php_cs.dist | 21 + .../swiftmailer/swiftmailer/.travis.yml | 30 + .../vendor/swiftmailer/swiftmailer/CHANGES | 366 + .../vendor/swiftmailer/swiftmailer/LICENSE | 19 + .../vendor/swiftmailer/swiftmailer/README.md | 19 + .../swiftmailer/swiftmailer/composer.json | 42 + .../swiftmailer/swiftmailer/doc/headers.rst | 621 + .../swiftmailer/swiftmailer/doc/index.rst | 12 + .../swiftmailer/doc/introduction.rst | 61 + .../swiftmailer/swiftmailer/doc/japanese.rst | 19 + .../swiftmailer/swiftmailer/doc/messages.rst | 947 + .../swiftmailer/swiftmailer/doc/plugins.rst | 337 + .../swiftmailer/swiftmailer/doc/sending.rst | 453 + .../swiftmailer/lib/classes/Swift.php | 78 + .../lib/classes/Swift/AddressEncoder.php | 25 + .../AddressEncoder/IdnAddressEncoder.php | 50 + .../AddressEncoder/Utf8AddressEncoder.php | 36 + .../classes/Swift/AddressEncoderException.php | 32 + .../lib/classes/Swift/Attachment.php | 54 + .../AbstractFilterableInputStream.php | 176 + .../Swift/ByteStream/ArrayByteStream.php | 178 + .../Swift/ByteStream/FileByteStream.php | 214 + .../ByteStream/TemporaryFileByteStream.php | 52 + .../lib/classes/Swift/CharacterReader.php | 67 + .../GenericFixedWidthReader.php | 97 + .../Swift/CharacterReader/UsAsciiReader.php | 84 + .../Swift/CharacterReader/Utf8Reader.php | 176 + .../classes/Swift/CharacterReaderFactory.php | 26 + .../SimpleCharacterReaderFactory.php | 124 + .../lib/classes/Swift/CharacterStream.php | 87 + .../CharacterStream/ArrayCharacterStream.php | 291 + .../CharacterStream/NgCharacterStream.php | 262 + .../lib/classes/Swift/ConfigurableSpool.php | 63 + .../lib/classes/Swift/DependencyContainer.php | 387 + .../lib/classes/Swift/DependencyException.php | 27 + .../lib/classes/Swift/EmbeddedFile.php | 53 + .../swiftmailer/lib/classes/Swift/Encoder.php | 28 + .../classes/Swift/Encoder/Base64Encoder.php | 58 + .../lib/classes/Swift/Encoder/QpEncoder.php | 300 + .../classes/Swift/Encoder/Rfc2231Encoder.php | 90 + .../lib/classes/Swift/Events/CommandEvent.php | 64 + .../classes/Swift/Events/CommandListener.php | 22 + .../lib/classes/Swift/Events/Event.php | 38 + .../classes/Swift/Events/EventDispatcher.php | 70 + .../classes/Swift/Events/EventListener.php | 18 + .../lib/classes/Swift/Events/EventObject.php | 61 + .../classes/Swift/Events/ResponseEvent.php | 64 + .../classes/Swift/Events/ResponseListener.php | 22 + .../lib/classes/Swift/Events/SendEvent.php | 126 + .../lib/classes/Swift/Events/SendListener.php | 27 + .../Swift/Events/SimpleEventDispatcher.php | 142 + .../Swift/Events/TransportChangeEvent.php | 27 + .../Swift/Events/TransportChangeListener.php | 37 + .../Swift/Events/TransportExceptionEvent.php | 43 + .../Events/TransportExceptionListener.php | 22 + .../lib/classes/Swift/FailoverTransport.php | 33 + .../lib/classes/Swift/FileSpool.php | 208 + .../lib/classes/Swift/FileStream.php | 24 + .../lib/classes/Swift/Filterable.php | 31 + .../lib/classes/Swift/IdGenerator.php | 22 + .../swiftmailer/lib/classes/Swift/Image.php | 43 + .../lib/classes/Swift/InputByteStream.php | 75 + .../lib/classes/Swift/IoException.php | 28 + .../lib/classes/Swift/KeyCache.php | 104 + .../classes/Swift/KeyCache/ArrayKeyCache.php | 197 + .../classes/Swift/KeyCache/DiskKeyCache.php | 294 + .../Swift/KeyCache/KeyCacheInputStream.php | 47 + .../classes/Swift/KeyCache/NullKeyCache.php | 113 + .../KeyCache/SimpleKeyCacheInputStream.php | 123 + .../classes/Swift/LoadBalancedTransport.php | 33 + .../swiftmailer/lib/classes/Swift/Mailer.php | 98 + .../Swift/Mailer/ArrayRecipientIterator.php | 53 + .../Swift/Mailer/RecipientIterator.php | 32 + .../lib/classes/Swift/MemorySpool.php | 110 + .../swiftmailer/lib/classes/Swift/Message.php | 279 + .../lib/classes/Swift/Mime/Attachment.php | 144 + .../classes/Swift/Mime/CharsetObserver.php | 24 + .../lib/classes/Swift/Mime/ContentEncoder.php | 34 + .../ContentEncoder/Base64ContentEncoder.php | 101 + .../ContentEncoder/NativeQpContentEncoder.php | 121 + .../ContentEncoder/NullContentEncoder.php | 79 + .../ContentEncoder/PlainContentEncoder.php | 164 + .../Mime/ContentEncoder/QpContentEncoder.php | 134 + .../ContentEncoder/QpContentEncoderProxy.php | 96 + .../Mime/ContentEncoder/RawContentEncoder.php | 65 + .../lib/classes/Swift/Mime/EmbeddedFile.php | 41 + .../classes/Swift/Mime/EncodingObserver.php | 22 + .../lib/classes/Swift/Mime/Header.php | 93 + .../lib/classes/Swift/Mime/HeaderEncoder.php | 24 + .../HeaderEncoder/Base64HeaderEncoder.php | 55 + .../Mime/HeaderEncoder/QpHeaderEncoder.php | 65 + .../Swift/Mime/Headers/AbstractHeader.php | 476 + .../classes/Swift/Mime/Headers/DateHeader.php | 113 + .../Mime/Headers/IdentificationHeader.php | 186 + .../Swift/Mime/Headers/MailboxHeader.php | 358 + .../Swift/Mime/Headers/OpenDKIMHeader.php | 135 + .../Mime/Headers/ParameterizedHeader.php | 255 + .../classes/Swift/Mime/Headers/PathHeader.php | 153 + .../Swift/Mime/Headers/UnstructuredHeader.php | 109 + .../lib/classes/Swift/Mime/IdGenerator.php | 54 + .../lib/classes/Swift/Mime/MimePart.php | 199 + .../Swift/Mime/SimpleHeaderFactory.php | 194 + .../classes/Swift/Mime/SimpleHeaderSet.php | 399 + .../lib/classes/Swift/Mime/SimpleMessage.php | 642 + .../classes/Swift/Mime/SimpleMimeEntity.php | 826 + .../lib/classes/Swift/MimePart.php | 45 + .../lib/classes/Swift/NullTransport.php | 26 + .../lib/classes/Swift/OutputByteStream.php | 46 + .../classes/Swift/Plugins/AntiFloodPlugin.php | 137 + .../Swift/Plugins/BandwidthMonitorPlugin.php | 154 + .../Swift/Plugins/Decorator/Replacements.php | 31 + .../classes/Swift/Plugins/DecoratorPlugin.php | 200 + .../Swift/Plugins/ImpersonatePlugin.php | 65 + .../lib/classes/Swift/Plugins/Logger.php | 36 + .../classes/Swift/Plugins/LoggerPlugin.php | 126 + .../Swift/Plugins/Loggers/ArrayLogger.php | 72 + .../Swift/Plugins/Loggers/EchoLogger.php | 58 + .../classes/Swift/Plugins/MessageLogger.php | 70 + .../Swift/Plugins/Pop/Pop3Connection.php | 31 + .../Swift/Plugins/Pop/Pop3Exception.php | 27 + .../Swift/Plugins/PopBeforeSmtpPlugin.php | 242 + .../Swift/Plugins/RedirectingPlugin.php | 201 + .../lib/classes/Swift/Plugins/Reporter.php | 31 + .../classes/Swift/Plugins/ReporterPlugin.php | 57 + .../Swift/Plugins/Reporters/HitReporter.php | 58 + .../Swift/Plugins/Reporters/HtmlReporter.php | 38 + .../lib/classes/Swift/Plugins/Sleeper.php | 24 + .../classes/Swift/Plugins/ThrottlerPlugin.php | 196 + .../lib/classes/Swift/Plugins/Timer.php | 24 + .../lib/classes/Swift/Preferences.php | 100 + .../Swift/ReplacementFilterFactory.php | 27 + .../classes/Swift/RfcComplianceException.php | 27 + .../lib/classes/Swift/SendmailTransport.php | 33 + .../swiftmailer/lib/classes/Swift/Signer.php | 19 + .../lib/classes/Swift/Signers/BodySigner.php | 31 + .../lib/classes/Swift/Signers/DKIMSigner.php | 682 + .../classes/Swift/Signers/DomainKeySigner.php | 504 + .../classes/Swift/Signers/HeaderSigner.php | 61 + .../classes/Swift/Signers/OpenDKIMSigner.php | 183 + .../lib/classes/Swift/Signers/SMimeSigner.php | 542 + .../lib/classes/Swift/SmtpTransport.php | 42 + .../swiftmailer/lib/classes/Swift/Spool.php | 53 + .../lib/classes/Swift/SpoolTransport.php | 33 + .../lib/classes/Swift/StreamFilter.php | 35 + .../ByteArrayReplacementFilter.php | 166 + .../StreamFilters/StringReplacementFilter.php | 70 + .../StringReplacementFilterFactory.php | 45 + .../lib/classes/Swift/SwiftException.php | 28 + .../lib/classes/Swift/Transport.php | 76 + .../Swift/Transport/AbstractSmtpTransport.php | 556 + .../Esmtp/Auth/CramMd5Authenticator.php | 75 + .../Esmtp/Auth/LoginAuthenticator.php | 45 + .../Esmtp/Auth/NTLMAuthenticator.php | 681 + .../Esmtp/Auth/PlainAuthenticator.php | 44 + .../Esmtp/Auth/XOAuth2Authenticator.php | 64 + .../Swift/Transport/Esmtp/AuthHandler.php | 268 + .../Swift/Transport/Esmtp/Authenticator.php | 36 + .../Transport/Esmtp/EightBitMimeHandler.php | 113 + .../Swift/Transport/Esmtp/SmtpUtf8Handler.php | 107 + .../classes/Swift/Transport/EsmtpHandler.php | 86 + .../Swift/Transport/EsmtpTransport.php | 446 + .../Swift/Transport/FailoverTransport.php | 103 + .../lib/classes/Swift/Transport/IoBuffer.php | 65 + .../Swift/Transport/LoadBalancedTransport.php | 0 .../classes/Swift/Transport/NullTransport.php | 98 + .../Swift/Transport/SendmailTransport.php | 158 + .../lib/classes/Swift/Transport/SmtpAgent.php | 36 + .../Swift/Transport/SpoolTransport.php | 120 + .../classes/Swift/Transport/StreamBuffer.php | 319 + .../lib/classes/Swift/TransportException.php | 28 + .../lib/dependency_maps/cache_deps.php | 23 + .../lib/dependency_maps/message_deps.php | 9 + .../lib/dependency_maps/mime_deps.php | 134 + .../lib/dependency_maps/transport_deps.php | 97 + .../swiftmailer/lib/mime_types.php | 1007 + .../swiftmailer/lib/preferences.php | 19 + .../swiftmailer/lib/swift_required.php | 22 + .../lib/swiftmailer_generate_mimes_config.php | 182 + .../vendor/symfony/polyfill-iconv/Iconv.php | 744 + .../vendor/symfony/polyfill-iconv/LICENSE | 19 + .../vendor/symfony/polyfill-iconv/README.md | 14 + .../Resources/charset/from.big5.php | 13719 +++ .../Resources/charset/from.cp037.php | Bin 0 -> 3779 bytes .../Resources/charset/from.cp1006.php | Bin 0 -> 3860 bytes .../Resources/charset/from.cp1026.php | Bin 0 -> 3779 bytes .../Resources/charset/from.cp424.php | Bin 0 -> 3210 bytes .../Resources/charset/from.cp437.php | Bin 0 -> 3841 bytes .../Resources/charset/from.cp500.php | Bin 0 -> 3779 bytes .../Resources/charset/from.cp737.php | Bin 0 -> 3834 bytes .../Resources/charset/from.cp775.php | Bin 0 -> 3815 bytes .../Resources/charset/from.cp850.php | Bin 0 -> 3809 bytes .../Resources/charset/from.cp852.php | Bin 0 -> 3808 bytes .../Resources/charset/from.cp855.php | Bin 0 -> 3809 bytes .../Resources/charset/from.cp856.php | Bin 0 -> 3194 bytes .../Resources/charset/from.cp857.php | Bin 0 -> 3763 bytes .../Resources/charset/from.cp860.php | Bin 0 -> 3840 bytes .../Resources/charset/from.cp861.php | Bin 0 -> 3841 bytes .../Resources/charset/from.cp862.php | Bin 0 -> 3841 bytes .../Resources/charset/from.cp863.php | Bin 0 -> 3841 bytes .../Resources/charset/from.cp864.php | Bin 0 -> 3779 bytes .../Resources/charset/from.cp865.php | Bin 0 -> 3841 bytes .../Resources/charset/from.cp866.php | Bin 0 -> 3831 bytes .../Resources/charset/from.cp869.php | Bin 0 -> 3676 bytes .../Resources/charset/from.cp874.php | Bin 0 -> 3410 bytes .../Resources/charset/from.cp875.php | Bin 0 -> 3776 bytes .../Resources/charset/from.cp932.php | Bin 0 -> 134095 bytes .../Resources/charset/from.cp936.php | Bin 0 -> 372283 bytes .../Resources/charset/from.cp949.php | Bin 0 -> 291504 bytes .../Resources/charset/from.cp950.php | Bin 0 -> 231436 bytes .../Resources/charset/from.iso-8859-1.php | Bin 0 -> 3779 bytes .../Resources/charset/from.iso-8859-10.php | Bin 0 -> 3780 bytes .../Resources/charset/from.iso-8859-11.php | Bin 0 -> 3746 bytes .../Resources/charset/from.iso-8859-13.php | Bin 0 -> 3783 bytes .../Resources/charset/from.iso-8859-14.php | Bin 0 -> 3801 bytes .../Resources/charset/from.iso-8859-15.php | Bin 0 -> 3780 bytes .../Resources/charset/from.iso-8859-16.php | Bin 0 -> 3782 bytes .../Resources/charset/from.iso-8859-2.php | Bin 0 -> 3779 bytes .../Resources/charset/from.iso-8859-3.php | Bin 0 -> 3674 bytes .../Resources/charset/from.iso-8859-4.php | Bin 0 -> 3779 bytes .../Resources/charset/from.iso-8859-5.php | Bin 0 -> 3780 bytes .../Resources/charset/from.iso-8859-6.php | Bin 0 -> 3104 bytes .../Resources/charset/from.iso-8859-7.php | Bin 0 -> 3692 bytes .../Resources/charset/from.iso-8859-8.php | Bin 0 -> 3242 bytes .../Resources/charset/from.iso-8859-9.php | Bin 0 -> 3779 bytes .../Resources/charset/from.koi8-r.php | Bin 0 -> 3835 bytes .../Resources/charset/from.koi8-u.php | Bin 0 -> 3827 bytes .../Resources/charset/from.us-ascii.php | Bin 0 -> 1859 bytes .../Resources/charset/from.windows-1250.php | Bin 0 -> 3721 bytes .../Resources/charset/from.windows-1251.php | Bin 0 -> 3782 bytes .../Resources/charset/from.windows-1252.php | Bin 0 -> 3721 bytes .../Resources/charset/from.windows-1253.php | Bin 0 -> 3542 bytes .../Resources/charset/from.windows-1254.php | Bin 0 -> 3691 bytes .../Resources/charset/from.windows-1255.php | Bin 0 -> 3454 bytes .../Resources/charset/from.windows-1256.php | Bin 0 -> 3800 bytes .../Resources/charset/from.windows-1257.php | Bin 0 -> 3616 bytes .../Resources/charset/from.windows-1258.php | Bin 0 -> 3662 bytes .../Resources/charset/translit.php | 4103 + .../symfony/polyfill-iconv/bootstrap.php | 88 + .../symfony/polyfill-iconv/bootstrap80.php | 80 + .../symfony/polyfill-iconv/composer.json | 38 + .../vendor/symfony/polyfill-intl-idn/Idn.php | 925 + .../vendor/symfony/polyfill-intl-idn/Info.php | 23 + .../vendor/symfony/polyfill-intl-idn/LICENSE | 19 + .../symfony/polyfill-intl-idn/README.md | 12 + .../Resources/unidata/DisallowedRanges.php | 375 + .../Resources/unidata/Regex.php | 24 + .../Resources/unidata/deviation.php | 8 + .../Resources/unidata/disallowed.php | 2638 + .../unidata/disallowed_STD3_mapped.php | 308 + .../unidata/disallowed_STD3_valid.php | 71 + .../Resources/unidata/ignored.php | 273 + .../Resources/unidata/mapped.php | 5778 + .../Resources/unidata/virama.php | 65 + .../symfony/polyfill-intl-idn/bootstrap.php | 145 + .../symfony/polyfill-intl-idn/bootstrap80.php | 128 + .../symfony/polyfill-intl-idn/composer.json | 44 + .../symfony/polyfill-intl-normalizer/LICENSE | 19 + .../polyfill-intl-normalizer/Normalizer.php | 310 + .../polyfill-intl-normalizer/README.md | 14 + .../Resources/stubs/Normalizer.php | 17 + .../unidata/canonicalComposition.php | 945 + .../unidata/canonicalDecomposition.php | 2065 + .../Resources/unidata/combiningClass.php | 876 + .../unidata/compatibilityDecomposition.php | 3695 + .../polyfill-intl-normalizer/bootstrap.php | 23 + .../polyfill-intl-normalizer/bootstrap80.php | 19 + .../polyfill-intl-normalizer/composer.json | 39 + .../vendor/symfony/polyfill-mbstring/LICENSE | 19 + .../symfony/polyfill-mbstring/Mbstring.php | 869 + .../symfony/polyfill-mbstring/README.md | 13 + .../Resources/unidata/lowerCase.php | 1397 + .../Resources/unidata/titleCaseRegexp.php | 5 + .../Resources/unidata/upperCase.php | 1414 + .../symfony/polyfill-mbstring/bootstrap.php | 147 + .../symfony/polyfill-mbstring/bootstrap80.php | 143 + .../symfony/polyfill-mbstring/composer.json | 38 + .../vendor/symfony/polyfill-php72/LICENSE | 19 + .../vendor/symfony/polyfill-php72/Php72.php | 217 + .../vendor/symfony/polyfill-php72/README.md | 28 + .../symfony/polyfill-php72/bootstrap.php | 57 + .../symfony/polyfill-php72/composer.json | 35 + user/plugins/flex-objects/.eslintrc | 170 + user/plugins/flex-objects/.gitignore | 3 + user/plugins/flex-objects/CHANGELOG.md | 469 + user/plugins/flex-objects/LICENSE | 21 + user/plugins/flex-objects/README.md | 289 + .../flex-objects/admin/pages/flex-objects.md | 7 + .../admin/templates/flex-objects.html.twig | 30 + .../admin/templates/flex-objects.json.twig | 1 + .../flex-objects/layouts/404.html.twig | 1 + .../layouts/accounts/partials/top.html.twig | 25 + .../types/default/buttons/add.html.twig | 3 + .../types/default/buttons/back.html.twig | 3 + .../default/buttons/configuration.html.twig | 7 + .../types/default/buttons/delete.html.twig | 3 + .../default/buttons/export-csv.html.twig | 3 + .../types/default/buttons/export.html.twig | 21 + .../types/default/buttons/languages.html.twig | 18 + .../default/buttons/preview-open.html.twig | 5 + .../types/default/buttons/preview.html.twig | 3 + .../types/default/buttons/save.html.twig | 4 + .../types/default/configure.html.twig | 103 + .../flex-objects/types/default/edit.html.twig | 121 + .../flex-objects/types/default/list.html.twig | 98 + .../types/default/list/list.html.twig | 112 + .../types/default/list/list_actions.html.twig | 39 + .../types/default/modals/remove.html.twig | 13 + .../types/default/preview.html.twig | 62 + .../default/titlebar/configure.html.twig | 32 + .../types/default/titlebar/edit.html.twig | 46 + .../types/default/titlebar/list.html.twig | 48 + .../types/default/titlebar/preview.html.twig | 30 + .../types/default/titlebar/types.html.twig | 22 + .../types/default/types.html.twig | 46 + .../types/pages/buttons/add.html.twig | 20 + .../types/pages/buttons/back.html.twig | 3 + .../types/pages/buttons/copy.html.twig | 4 + .../types/pages/buttons/delete.html.twig | 3 + .../types/pages/buttons/move.html.twig | 6 + .../types/pages/buttons/nav-child.html.twig | 9 + .../types/pages/buttons/nav-next.html.twig | 9 + .../types/pages/buttons/nav-parent.html.twig | 9 + .../types/pages/buttons/nav-prev.html.twig | 9 + .../types/pages/buttons/preview.html.twig | 5 + .../types/pages/buttons/save.html.twig | 23 + .../flex-objects/types/pages/edit.html.twig | 236 + .../flex-objects/types/pages/list.html.twig | 29 + .../types/pages/list/columns.html.twig | 155 + .../types/pages/list/list.html.twig | 41 + .../types/pages/preview.html.twig | 16 + .../types/user-accounts/configure.html.twig | 9 + .../types/user-accounts/edit.html.twig | 9 + .../types/user-accounts/list.html.twig | 7 + .../types/user-groups/configure.html.twig | 9 + .../types/user-groups/list.html.twig | 7 + .../flex-objects/flex-objects.html.twig | 69 + .../save-redirect/save-redirect.html.twig | 37 + .../flex-objects/app/columns/finder.js | 425 + .../plugins/flex-objects/app/columns/index.js | 102 + .../plugins/flex-objects/app/filters/index.js | 46 + .../plugins/flex-objects/app/filters/panel.js | 15 + user/plugins/flex-objects/app/list/App.vue | 27 + .../app/list/VuetableCssConfig.js | 29 + .../app/list/components/ContentLoader.vue | 48 + .../app/list/components/FilterBar.vue | 66 + .../app/list/components/Table.vue | 94 + user/plugins/flex-objects/app/list/index.js | 19 + user/plugins/flex-objects/app/main.js | 3 + user/plugins/flex-objects/app/utils/finder.js | 393 + .../flex-objects/app/utils/get-filters.js | 34 + .../flex-objects/app/utils/indeterminate.js | 44 + .../assets/flex-objects-directory.png | Bin 0 -> 137785 bytes .../flex-objects/assets/flex-objects-edit.png | Bin 0 -> 159728 bytes .../flex-objects/assets/flex-objects-list.png | Bin 0 -> 239360 bytes .../assets/flex-objects-options.png | Bin 0 -> 256165 bytes .../flex-objects/assets/flex-objects-site.png | Bin 0 -> 253928 bytes user/plugins/flex-objects/blueprints.yaml | 55 + .../flex-objects/blueprints/flex-objects.yaml | 16 + .../blueprints/flex-objects/contacts.yaml | 158 + .../blueprints/flex-objects/pages.yaml | 22 + .../flex-objects/user-accounts.yaml | 7 + .../blueprints/flex-objects/user-groups.yaml | 7 + .../blueprints/pages/flex-objects.yaml | 21 + .../classes/Admin/AdminController.php | 1911 + .../Controllers/AbstractController.php | 359 + .../classes/Controllers/MediaController.php | 504 + .../classes/Controllers/ObjectController.php | 501 + user/plugins/flex-objects/classes/Flex.php | 444 + .../flex-objects/classes/FlexFormFactory.php | 80 + .../flex-objects/classes/FlexRouter.php | 64 + .../flex-objects/classes/Table/DataTable.php | 410 + .../cli/FlexConvertDataCommand.php | 140 + user/plugins/flex-objects/composer.json | 32 + user/plugins/flex-objects/composer.lock | 25 + user/plugins/flex-objects/css/admin.css | 97 + user/plugins/flex-objects/css/admin.css.map | 11 + user/plugins/flex-objects/css/admin.min.css | 1 + user/plugins/flex-objects/css/site.css | 27 + user/plugins/flex-objects/css/site.css.map | 11 + user/plugins/flex-objects/css/site.min.css | 1 + .../data/flex-objects/contacts.json | 6262 ++ user/plugins/flex-objects/flex-objects.php | 785 + user/plugins/flex-objects/flex-objects.yaml | 14 + user/plugins/flex-objects/js/flex-objects.js | 905 + user/plugins/flex-objects/js/list.min.js | 2 + user/plugins/flex-objects/languages/de.yaml | 60 + user/plugins/flex-objects/languages/en.yaml | 72 + user/plugins/flex-objects/languages/ru.yaml | 12 + user/plugins/flex-objects/package.json | 51 + user/plugins/flex-objects/permissions.yaml | 45 + user/plugins/flex-objects/scss/_preset.scss | 69 + user/plugins/flex-objects/scss/admin.scss | 3 + .../flex-objects/scss/plugin/_admin.scss | 215 + .../flex-objects/scss/plugin/_site.scss | 68 + user/plugins/flex-objects/scss/site.scss | 2 + .../templates/flex-edit.html.twig | 42 + .../templates/flex-objects.html.twig | 21 + .../directories/default.html.twig | 30 + .../templates/flex-objects/page.html.twig | 37 + .../flex-objects/views/404.html.twig | 1 + .../flex-objects/views/directories.html.twig | 12 + .../flex-objects/views/directory.html.twig | 24 + .../flex-objects/views/object.html.twig | 23 + .../contacts/collection/default.html.twig | 26 + .../flex/contacts/object/default.html.twig | 15 + .../contacts/object/list-default.html.twig | 15 + .../collection/default.html.twig | 25 + .../user-accounts/object/default.html.twig | 17 + .../object/list-default.html.twig | 27 + .../user-groups/collection/default.html.twig | 25 + .../flex/user-groups/object/default.html.twig | 14 + .../user-groups/object/list-default.html.twig | 16 + .../fields/filepicker/filepicker.html.twig | 17 + .../forms/fields/list/list.html.twig | 177 + .../fields/pagemedia/pagemedia.html.twig | 89 + .../forms/fields/pagemedia/template.html.twig | 33 + user/plugins/flex-objects/vendor/autoload.php | 7 + .../vendor/composer/ClassLoader.php | 445 + .../vendor/composer/InstalledVersions.php | 219 + .../flex-objects/vendor/composer/LICENSE | 21 + .../vendor/composer/autoload_classmap.php | 11 + .../vendor/composer/autoload_namespaces.php | 9 + .../vendor/composer/autoload_psr4.php | 10 + .../vendor/composer/autoload_real.php | 57 + .../vendor/composer/autoload_static.php | 37 + .../vendor/composer/installed.json | 5 + .../vendor/composer/installed.php | 24 + .../vendor/composer/platform_check.php | 26 + user/plugins/flex-objects/watch.sh | 27 + user/plugins/flex-objects/webpack.conf.js | 75 + user/plugins/flex-objects/yarn.lock | 3271 + user/plugins/form/.eslintrc | 165 + user/plugins/form/.gitignore | 5 + user/plugins/form/CHANGELOG.md | 1158 + user/plugins/form/LICENSE | 21 + user/plugins/form/README.md | 39 + user/plugins/form/app/fields/array.js | 200 + user/plugins/form/app/fields/file.js | 340 + user/plugins/form/app/fields/form.js | 94 + user/plugins/form/app/fields/index.js | 7 + user/plugins/form/app/fields/media.js | 174 + user/plugins/form/app/fields/tabs.js | 14 + user/plugins/form/app/main.js | 4 + user/plugins/form/app/utils/keep-alive.js | 17 + user/plugins/form/assets/dropzone.min.css | 1 + user/plugins/form/assets/form-styles.css | 80 + user/plugins/form/assets/form-styles.min.css | 1 + user/plugins/form/assets/form.min.js | 41 + user/plugins/form/assets/form.vendor.js | 24 + .../form/assets/object.assign.polyfill.js | 29 + user/plugins/form/assets/signature_pad.js | 617 + user/plugins/form/blueprints.yaml | 198 + user/plugins/form/classes/Form.php | 1422 + user/plugins/form/classes/FormFactory.php | 38 + user/plugins/form/classes/Forms.php | 129 + user/plugins/form/classes/TwigExtension.php | 155 + user/plugins/form/composer.json | 40 + user/plugins/form/composer.lock | 77 + user/plugins/form/form.php | 1167 + user/plugins/form/form.yaml | 20 + user/plugins/form/gulpfile.js | 84 + user/plugins/form/hebe.json | 15 + user/plugins/form/languages.yaml | 897 + user/plugins/form/package.json | 45 + user/plugins/form/scss/form-styles.scss | 289 + .../form/templates/form-messages.html.twig | 2 + .../form/templates/form-messages.json.twig | 2 + user/plugins/form/templates/form.html.twig | 8 + user/plugins/form/templates/form.json.twig | 1 + .../plugins/form/templates/formdata.html.twig | 20 + .../form/templates/forms/ajax.json.twig | 5 + .../form/templates/forms/data.html.twig | 1 + .../form/templates/forms/data.txt.twig | 1 + .../templates/forms/default/data.html.twig | 77 + .../templates/forms/default/data.txt.twig | 21 + .../templates/forms/default/field.html.twig | 158 + .../templates/forms/default/fields.html.twig | 17 + .../templates/forms/default/form.html.twig | 206 + .../forms/default/toggleable.html.twig | 9 + .../forms/dropzone/template.html.twig | 39 + .../form/templates/forms/field.html.twig | 9 + .../forms/fields/array/array.html.twig | 99 + .../forms/fields/avatar/avatar.html.twig | 10 + .../forms/fields/captcha/captcha.html.twig | 100 + .../forms/fields/checkbox/checkbox.html.twig | 46 + .../fields/checkboxes/checkboxes.html.twig | 42 + .../forms/fields/color/color.html.twig | 6 + .../forms/fields/column/column.html.twig | 8 + .../forms/fields/columns/columns.html.twig | 7 + .../fields/conditional/conditional.html.twig | 19 + .../forms/fields/date/date.html.twig | 8 + .../forms/fields/date/edit_list.html.twig | 1 + .../forms/fields/datetime/datetime.html.twig | 2 + .../forms/fields/datetime/edit_list.html.twig | 1 + .../forms/fields/display/display.html.twig | 21 + .../forms/fields/email/email.html.twig | 10 + .../forms/fields/fieldset/fieldset.html.twig | 12 + .../forms/fields/file/file.html.twig | 124 + .../forms/fields/formname/formname.html.twig | 5 + .../forms/fields/formtask/formtask.html.twig | 8 + .../forms/fields/hidden/hidden.html.twig | 15 + .../forms/fields/honeypot/honeypot.html.twig | 13 + .../forms/fields/ignore/ignore.html.twig | 0 .../templates/forms/fields/key/key.html.twig | 28 + .../forms/fields/month/month.html.twig | 6 + .../forms/fields/nonce/nonce.html.twig | 1 + .../forms/fields/number/number.html.twig | 9 + .../forms/fields/password/password.html.twig | 8 + .../forms/fields/radio/radio.html.twig | 24 + .../forms/fields/range/range.html.twig | 9 + .../forms/fields/section/section.html.twig | 21 + .../forms/fields/select/select.html.twig | 71 + .../select_optgroup/select_optgroup.html.twig | 2 + .../fields/signature/signature.html.twig | 105 + .../forms/fields/spacer/spacer.html.twig | 19 + .../forms/fields/switch/switch.html.twig | 1 + .../templates/forms/fields/tab/tab.html.twig | 8 + .../forms/fields/tabs/tabs.html.twig | 60 + .../templates/forms/fields/tel/tel.html.twig | 9 + .../forms/fields/text/edit_list.html.twig | 7 + .../forms/fields/text/text.html.twig | 39 + .../forms/fields/textarea/textarea.html.twig | 51 + .../forms/fields/time/edit_list.html.twig | 1 + .../forms/fields/time/time.html.twig | 6 + .../forms/fields/toggle/edit_list.html.twig | 5 + .../forms/fields/toggle/toggle.html.twig | 52 + .../forms/fields/uniqueid/uniqueid.html.twig | 5 + .../forms/fields/url/edit_list.html.twig | 1 + .../templates/forms/fields/url/url.html.twig | 9 + .../forms/fields/value/value.html.twig | 18 + .../forms/fields/week/week.html.twig | 6 + .../form/templates/forms/form.html.twig | 29 + .../templates/forms/layouts/button.html.twig | 4 + .../layouts/button/default-button.html.twig | 12 + .../forms/layouts/field-variables.html.twig | 1 + .../templates/forms/layouts/field.html.twig | 4 + .../layouts/field/default-field.html.twig | 53 + .../templates/forms/layouts/form.html.twig | 4 + .../forms/layouts/form/default-form.html.twig | 8 + .../form/templates/modular/form.html.twig | 4 + .../partials/form-messages.html.twig | 6 + .../partials/form-messages.json.twig | 3 + user/plugins/form/vendor/autoload.php | 7 + .../form/vendor/composer/ClassLoader.php | 445 + .../vendor/composer/InstalledVersions.php | 228 + user/plugins/form/vendor/composer/LICENSE | 21 + .../vendor/composer/autoload_classmap.php | 11 + .../vendor/composer/autoload_namespaces.php | 9 + .../form/vendor/composer/autoload_psr4.php | 11 + .../form/vendor/composer/autoload_real.php | 57 + .../form/vendor/composer/autoload_static.php | 45 + .../form/vendor/composer/installed.json | 61 + .../form/vendor/composer/installed.php | 33 + .../form/vendor/composer/platform_check.php | 26 + .../.github/ISSUE_TEMPLATE/bug_report.md | 28 + .../form/vendor/google/recaptcha/.gitignore | 7 + .../form/vendor/google/recaptcha/.travis.yml | 33 + .../vendor/google/recaptcha/ARCHITECTURE.md | 64 + .../vendor/google/recaptcha/CONTRIBUTING.md | 49 + .../form/vendor/google/recaptcha/LICENSE | 29 + .../form/vendor/google/recaptcha/README.md | 140 + .../form/vendor/google/recaptcha/app.yaml | 8 + .../vendor/google/recaptcha/composer.json | 39 + .../recaptcha/examples/appengine-https.php | 42 + .../google/recaptcha/examples/config.php.dist | 46 + .../google/recaptcha/examples/examples.css | 37 + .../examples/google0afd8760fd68f119.html | 1 + .../google/recaptcha/examples/index.php | 79 + .../recaptcha-content-security-policy.php | 152 + .../recaptcha-v2-checkbox-explicit.php | 148 + .../examples/recaptcha-v2-checkbox.php | 139 + .../examples/recaptcha-v2-invisible.php | 141 + .../examples/recaptcha-v3-request-scores.php | 120 + .../examples/recaptcha-v3-verify.php | 59 + .../google/recaptcha/examples/robots.txt | 2 + .../vendor/google/recaptcha/phpunit.xml.dist | 20 + .../recaptcha/src/ReCaptcha/ReCaptcha.php | 269 + .../recaptcha/src/ReCaptcha/RequestMethod.php | 50 + .../src/ReCaptcha/RequestMethod/Curl.php | 82 + .../src/ReCaptcha/RequestMethod/CurlPost.php | 104 + .../src/ReCaptcha/RequestMethod/Post.php | 88 + .../src/ReCaptcha/RequestMethod/Socket.php | 112 + .../ReCaptcha/RequestMethod/SocketPost.php | 108 + .../src/ReCaptcha/RequestParameters.php | 111 + .../recaptcha/src/ReCaptcha/Response.php | 218 + .../vendor/google/recaptcha/src/autoload.php | 69 + .../tests/ReCaptcha/ReCaptchaTest.php | 198 + .../ReCaptcha/RequestMethod/CurlPostTest.php | 123 + .../ReCaptcha/RequestMethod/PostTest.php | 149 + .../RequestMethod/SocketPostTest.php | 136 + .../tests/ReCaptcha/RequestParametersTest.php | 70 + .../tests/ReCaptcha/ResponseTest.php | 173 + user/plugins/form/webpack.conf.js | 65 + user/plugins/form/yarn.lock | 7407 ++ user/plugins/login/.gitignore | 2 + user/plugins/login/CHANGELOG.md | 834 + user/plugins/login/LICENSE | 21 + user/plugins/login/README.md | 556 + user/plugins/login/blueprints.yaml | 462 + user/plugins/login/classes/Controller.php | 758 + .../login/classes/Events/UserLoginEvent.php | 397 + .../login/classes/Invitations/Invitation.php | 59 + .../login/classes/Invitations/Invitations.php | 165 + user/plugins/login/classes/Login.php | 925 + user/plugins/login/classes/LoginCache.php | 105 + user/plugins/login/classes/RateLimiter.php | 110 + .../login/classes/RememberMe/RememberMe.php | 43 + .../login/classes/RememberMe/TokenStorage.php | 172 + .../classes/TwoFactorAuth/BaconQrProvider.php | 32 + .../classes/TwoFactorAuth/TwoFactorAuth.php | 76 + .../login/cli/ChangePasswordCommand.php | 177 + .../login/cli/ChangeUserStateCommand.php | 157 + user/plugins/login/cli/LookupUserCommand.php | 94 + user/plugins/login/cli/NewUserCommand.php | 324 + user/plugins/login/composer.json | 49 + user/plugins/login/composer.lock | 247 + user/plugins/login/css/login.css | 86 + user/plugins/login/hebe.json | 15 + user/plugins/login/js/2fa.js | 34 + user/plugins/login/languages/de.yaml | 139 + user/plugins/login/languages/en.yaml | 161 + user/plugins/login/languages/es.yaml | 126 + user/plugins/login/languages/fr.yaml | 97 + user/plugins/login/languages/hr.yaml | 54 + user/plugins/login/languages/hu.yaml | 35 + user/plugins/login/languages/lt.yaml | 154 + user/plugins/login/languages/no.yaml | 73 + user/plugins/login/languages/pt-BR.yaml | 136 + user/plugins/login/languages/ro.yaml | 97 + user/plugins/login/languages/ru.yaml | 145 + user/plugins/login/languages/uk.yaml | 145 + user/plugins/login/languages/zh.yaml | 136 + user/plugins/login/login.php | 1245 + user/plugins/login/login.yaml | 63 + user/plugins/login/pages/forgot.md | 22 + user/plugins/login/pages/login.md | 42 + user/plugins/login/pages/profile.md | 81 + user/plugins/login/pages/register.md | 60 + user/plugins/login/pages/reset.md | 35 + user/plugins/login/pages/unauthorized.md | 7 + user/plugins/login/templates/forgot.html.twig | 6 + .../fields/2fa_secret/2fa_secret.html.twig | 31 + user/plugins/login/templates/login.html.twig | 6 + user/plugins/login/templates/login.json.twig | 5 + .../templates/partials/forgot-form.html.twig | 29 + .../templates/partials/login-form.html.twig | 74 + .../templates/partials/login-status.html.twig | 5 + .../templates/partials/login-twofa.html.twig | 32 + .../templates/partials/reset-form.html.twig | 30 + .../plugins/login/templates/profile.html.twig | 9 + .../plugins/login/templates/profile.json.twig | 1 + .../login/templates/register.html.twig | 1 + user/plugins/login/templates/reset.html.twig | 8 + .../login/templates/unauthorized.html.twig | 1 + user/plugins/login/vendor/autoload.php | 7 + .../vendor/bacon/bacon-qr-code/.gitignore | 9 + .../vendor/bacon/bacon-qr-code/.travis.yml | 14 + .../vendor/bacon/bacon-qr-code/CHANGELOG.md | 37 + .../login/vendor/bacon/bacon-qr-code/LICENSE | 22 + .../vendor/bacon/bacon-qr-code/Module.php | 37 + .../vendor/bacon/bacon-qr-code/README.md | 24 + .../bacon/bacon-qr-code/autoload_classmap.php | 43 + .../bacon/bacon-qr-code/autoload_function.php | 12 + .../bacon/bacon-qr-code/autoload_register.php | 2 + .../vendor/bacon/bacon-qr-code/composer.json | 29 + .../src/BaconQrCode/Common/AbstractEnum.php | 115 + .../src/BaconQrCode/Common/BitArray.php | 435 + .../src/BaconQrCode/Common/BitMatrix.php | 350 + .../src/BaconQrCode/Common/BitUtils.php | 51 + .../BaconQrCode/Common/CharacterSetEci.php | 134 + .../src/BaconQrCode/Common/EcBlock.php | 65 + .../src/BaconQrCode/Common/EcBlocks.php | 101 + .../Common/ErrorCorrectionLevel.php | 62 + .../BaconQrCode/Common/FormatInformation.php | 236 + .../src/BaconQrCode/Common/Mode.php | 70 + .../BaconQrCode/Common/ReedSolomonCodec.php | 476 + .../src/BaconQrCode/Common/Version.php | 687 + .../src/BaconQrCode/Encoder/BlockPair.php | 64 + .../src/BaconQrCode/Encoder/ByteMatrix.php | 158 + .../src/BaconQrCode/Encoder/Encoder.php | 687 + .../src/BaconQrCode/Encoder/MaskUtil.php | 291 + .../src/BaconQrCode/Encoder/MatrixUtil.php | 580 + .../src/BaconQrCode/Encoder/QrCode.php | 201 + .../Exception/ExceptionInterface.php | 14 + .../Exception/InvalidArgumentException.php | 14 + .../Exception/OutOfBoundsException.php | 14 + .../Exception/RuntimeException.php | 14 + .../Exception/UnexpectedValueException.php | 14 + .../BaconQrCode/Exception/WriterException.php | 14 + .../src/BaconQrCode/Renderer/Color/Cmyk.php | 160 + .../Renderer/Color/ColorInterface.php | 37 + .../src/BaconQrCode/Renderer/Color/Gray.php | 84 + .../src/BaconQrCode/Renderer/Color/Rgb.php | 148 + .../Renderer/Image/AbstractRenderer.php | 338 + .../Image/Decorator/DecoratorInterface.php | 63 + .../Image/Decorator/FinderPattern.php | 210 + .../src/BaconQrCode/Renderer/Image/Eps.php | 152 + .../src/BaconQrCode/Renderer/Image/Png.php | 115 + .../Renderer/Image/RendererInterface.php | 61 + .../src/BaconQrCode/Renderer/Image/Svg.php | 146 + .../Renderer/RendererInterface.php | 26 + .../src/BaconQrCode/Renderer/Text/Html.php | 91 + .../src/BaconQrCode/Renderer/Text/Plain.php | 150 + .../bacon-qr-code/src/BaconQrCode/Writer.php | 105 + .../bacon-qr-code/src/Common/BitArray.php | 372 + .../bacon-qr-code/src/Common/BitMatrix.php | 313 + .../bacon-qr-code/src/Common/BitUtils.php | 41 + .../src/Common/CharacterSetEci.php | 180 + .../bacon-qr-code/src/Common/EcBlock.php | 49 + .../bacon-qr-code/src/Common/EcBlocks.php | 74 + .../src/Common/ErrorCorrectionLevel.php | 63 + .../src/Common/FormatInformation.php | 203 + .../bacon/bacon-qr-code/src/Common/Mode.php | 76 + .../src/Common/ReedSolomonCodec.php | 468 + .../bacon-qr-code/src/Common/Version.php | 596 + .../bacon-qr-code/src/Encoder/BlockPair.php | 58 + .../bacon-qr-code/src/Encoder/ByteMatrix.php | 150 + .../bacon-qr-code/src/Encoder/Encoder.php | 652 + .../bacon-qr-code/src/Encoder/MaskUtil.php | 271 + .../bacon-qr-code/src/Encoder/MatrixUtil.php | 513 + .../bacon-qr-code/src/Encoder/QrCode.php | 141 + .../src/Exception/ExceptionInterface.php | 10 + .../Exception/InvalidArgumentException.php | 8 + .../src/Exception/OutOfBoundsException.php | 8 + .../src/Exception/RuntimeException.php | 8 + .../Exception/UnexpectedValueException.php | 8 + .../src/Exception/WriterException.php | 8 + .../src/Renderer/Color/Alpha.php | 57 + .../bacon-qr-code/src/Renderer/Color/Cmyk.php | 103 + .../src/Renderer/Color/ColorInterface.php | 22 + .../bacon-qr-code/src/Renderer/Color/Gray.php | 46 + .../bacon-qr-code/src/Renderer/Color/Rgb.php | 88 + .../src/Renderer/Eye/CompositeEye.php | 38 + .../src/Renderer/Eye/EyeInterface.php | 26 + .../src/Renderer/Eye/ModuleEye.php | 54 + .../src/Renderer/Eye/SimpleCircleEye.php | 54 + .../src/Renderer/Eye/SquareEye.php | 53 + .../src/Renderer/Image/EpsImageBackEnd.php | 376 + .../Renderer/Image/ImageBackEndInterface.php | 87 + .../Renderer/Image/ImagickImageBackEnd.php | 339 + .../src/Renderer/Image/SvgImageBackEnd.php | 369 + .../Renderer/Image/TransformationMatrix.php | 67 + .../src/Renderer/ImageRenderer.php | 152 + .../src/Renderer/Module/DotsModule.php | 63 + .../src/Renderer/Module/EdgeIterator/Edge.php | 100 + .../Module/EdgeIterator/EdgeIterator.php | 169 + .../src/Renderer/Module/ModuleInterface.php | 18 + .../src/Renderer/Module/RoundnessModule.php | 129 + .../src/Renderer/Module/SquareModule.php | 47 + .../bacon-qr-code/src/Renderer/Path/Close.php | 29 + .../bacon-qr-code/src/Renderer/Path/Curve.php | 92 + .../src/Renderer/Path/EllipticArc.php | 278 + .../bacon-qr-code/src/Renderer/Path/Line.php | 41 + .../bacon-qr-code/src/Renderer/Path/Move.php | 41 + .../src/Renderer/Path/OperationInterface.php | 12 + .../bacon-qr-code/src/Renderer/Path/Path.php | 106 + .../src/Renderer/PlainTextRenderer.php | 86 + .../src/Renderer/RendererInterface.php | 11 + .../src/Renderer/RendererStyle/EyeFill.php | 74 + .../src/Renderer/RendererStyle/Fill.php | 168 + .../src/Renderer/RendererStyle/Gradient.php | 46 + .../Renderer/RendererStyle/GradientType.php | 22 + .../Renderer/RendererStyle/RendererStyle.php | 90 + .../vendor/bacon/bacon-qr-code/src/Writer.php | 68 + .../tests/BaconQrCode/Common/BitArrayTest.php | 201 + .../BaconQrCode/Common/BitMatrixTest.php | 119 + .../tests/BaconQrCode/Common/BitUtilsTest.php | 30 + .../Common/ErrorCorrectionLevelTest.php | 40 + .../Common/FormatInformationTest.php | 104 + .../tests/BaconQrCode/Common/ModeTest.php | 42 + .../Common/ReedSolomonCodecTest.php | 111 + .../tests/BaconQrCode/Common/VersionTest.php | 88 + .../tests/BaconQrCode/Encoder/EncoderTest.php | 468 + .../BaconQrCode/Encoder/MaskUtilTest.php | 281 + .../BaconQrCode/Encoder/MatrixUtilTest.php | 336 + .../BaconQrCode/Renderer/Text/HtmlTest.php | 99 + .../BaconQrCode/Renderer/Text/TextTest.php | 149 + .../bacon/bacon-qr-code/tests/bootstrap.php | 10 + .../bacon/bacon-qr-code/tests/phpunit.xml | 11 + .../login/vendor/birke/rememberme/.gitignore | 4 + .../login/vendor/birke/rememberme/LICENSE | 21 + .../login/vendor/birke/rememberme/README.md | 40 + .../vendor/birke/rememberme/composer.json | 24 + .../birke/rememberme/example/action.php | 119 + .../birke/rememberme/example/css/style.css | 157 + .../vendor/birke/rememberme/example/index.php | 42 + .../example/templates/cookie_was_stolen.php | 4 + .../rememberme/example/templates/login.php | 14 + .../example/templates/user_is_logged_in.php | 31 + .../login/vendor/birke/rememberme/phpunit.xml | 7 + .../src/Rememberme/Authenticator.php | 293 + .../rememberme/src/Rememberme/Cookie.php | 104 + .../rememberme/src/Rememberme/Storage/DB.php | 145 + .../src/Rememberme/Storage/File.php | 119 + .../rememberme/src/Rememberme/Storage/PDO.php | 121 + .../Rememberme/Storage/StorageInterface.php | 66 + .../birke/rememberme/test/CookieTest.php | 31 + .../birke/rememberme/test/RemembermeTest.php | 352 + .../birke/rememberme/test/Storage/PDO.php | 98 + .../birke/rememberme/test/Storage/tokens.xml | 8 + .../birke/rememberme/test/bootstrap.php | 3 + .../login/vendor/composer/ClassLoader.php | 445 + .../vendor/composer/InstalledVersions.php | 255 + user/plugins/login/vendor/composer/LICENSE | 21 + .../vendor/composer/autoload_classmap.php | 11 + .../login/vendor/composer/autoload_files.php | 10 + .../vendor/composer/autoload_namespaces.php | 10 + .../login/vendor/composer/autoload_psr4.php | 13 + .../login/vendor/composer/autoload_real.php | 75 + .../login/vendor/composer/autoload_static.php | 73 + .../login/vendor/composer/installed.json | 227 + .../login/vendor/composer/installed.php | 60 + .../login/vendor/composer/platform_check.php | 26 + .../login/vendor/dasprid/enum/.coveralls.yml | 2 + .../login/vendor/dasprid/enum/.gitignore | 3 + .../login/vendor/dasprid/enum/.travis.yml | 41 + .../login/vendor/dasprid/enum/README.md | 164 + .../login/vendor/dasprid/enum/composer.json | 30 + .../login/vendor/dasprid/enum/phpcs.xml | 30 + .../vendor/dasprid/enum/phpunit.xml.dist | 17 + .../vendor/dasprid/enum/src/AbstractEnum.php | 241 + .../login/vendor/dasprid/enum/src/EnumMap.php | 375 + .../Exception/CloneNotSupportedException.php | 10 + .../enum/src/Exception/ExceptionInterface.php | 10 + .../src/Exception/ExpectationException.php | 10 + .../Exception/IllegalArgumentException.php | 10 + .../enum/src/Exception/MismatchException.php | 10 + .../SerializeNotSupportedException.php | 10 + .../UnserializeNotSupportedException.php | 10 + .../vendor/dasprid/enum/src/NullValue.php | 55 + .../dasprid/enum/test/AbstractEnumTest.php | 121 + .../vendor/dasprid/enum/test/EnumMapTest.php | 243 + .../dasprid/enum/test/NullValueTest.php | 31 + .../login/vendor/dasprid/enum/test/Planet.php | 73 + .../vendor/dasprid/enum/test/WeekDay.php | 26 + .../paragonie/random_compat/CHANGELOG.md | 290 + .../vendor/paragonie/random_compat/LICENSE | 22 + .../paragonie/random_compat/RATIONALE.md | 42 + .../vendor/paragonie/random_compat/README.md | 176 + .../paragonie/random_compat/SECURITY.md | 108 + .../paragonie/random_compat/build-phar.sh | 5 + .../paragonie/random_compat/composer.json | 35 + .../dist/random_compat.phar.pubkey | 5 + .../dist/random_compat.phar.pubkey.asc | 11 + .../random_compat/lib/byte_safe_strings.php | 181 + .../random_compat/lib/cast_to_int.php | 74 + .../random_compat/lib/error_polyfill.php | 49 + .../paragonie/random_compat/lib/random.php | 247 + .../lib/random_bytes_com_dotnet.php | 88 + .../lib/random_bytes_dev_urandom.php | 150 + .../lib/random_bytes_libsodium.php | 88 + .../lib/random_bytes_libsodium_legacy.php | 92 + .../random_compat/lib/random_bytes_mcrypt.php | 77 + .../lib/random_bytes_openssl.php | 86 + .../random_compat/lib/random_int.php | 190 + .../random_compat/other/build_phar.php | 57 + .../random_compat/other/ide_stubs/COM.php | 20 + .../random_compat/other/ide_stubs/README.md | 7 + .../other/ide_stubs/com_exception.php | 11 + .../other/ide_stubs/libsodium.php | 90 + .../random_compat/psalm-autoload.php | 9 + .../vendor/paragonie/random_compat/psalm.xml | 15 + .../twofactorauth/.github/FUNDING.yml | 4 + .../twofactorauth/.github/workflows/test.yml | 27 + .../vendor/robthree/twofactorauth/.gitignore | 192 + .../vendor/robthree/twofactorauth/LICENSE | 22 + .../vendor/robthree/twofactorauth/README.md | 209 + .../twofactorauth/TwoFactorAuth.phpproj | 70 + .../robthree/twofactorauth/TwoFactorAuth.sln | 22 + .../robthree/twofactorauth/composer.json | 49 + .../robthree/twofactorauth/demo/demo.php | 35 + .../robthree/twofactorauth/demo/loader.php | 50 + .../lib/Providers/Qr/BaconQrCodeProvider.php | 152 + .../Providers/Qr/BaseHTTPQRCodeProvider.php | 33 + .../Providers/Qr/EndroidQrCodeProvider.php | 70 + .../Qr/EndroidQrCodeWithLogoProvider.php | 35 + .../lib/Providers/Qr/IQRCodeProvider.php | 24 + .../Qr/ImageChartsQRCodeProvider.php | 60 + .../lib/Providers/Qr/QRException.php | 7 + .../lib/Providers/Qr/QRServerProvider.php | 108 + .../lib/Providers/Qr/QRicketProvider.php | 76 + .../lib/Providers/Rng/CSRNGProvider.php | 22 + .../lib/Providers/Rng/HashRNGProvider.php | 43 + .../lib/Providers/Rng/IRNGProvider.php | 18 + .../lib/Providers/Rng/MCryptRNGProvider.php | 37 + .../lib/Providers/Rng/OpenSSLRNGProvider.php | 40 + .../lib/Providers/Rng/RNGException.php | 7 + .../lib/Providers/Time/HttpTimeProvider.php | 71 + .../lib/Providers/Time/ITimeProvider.php | 11 + .../Time/LocalMachineTimeProvider.php | 11 + .../lib/Providers/Time/NTPTimeProvider.php | 70 + .../lib/Providers/Time/TimeException.php | 7 + .../twofactorauth/lib/TwoFactorAuth.php | 360 + .../lib/TwoFactorAuthException.php | 7 + .../vendor/robthree/twofactorauth/logo.png | Bin 0 -> 2636 bytes .../multifactorauthforeveryone.png | Bin 0 -> 20735 bytes .../vendor/robthree/twofactorauth/phpunit.xml | 26 + .../tests/MightNotMakeAssertions.php | 26 + .../Providers/Qr/IQRCodeProviderTest.php | 56 + .../tests/Providers/Qr/TestQrProvider.php | 24 + .../tests/Providers/Rng/CSRNGProviderTest.php | 30 + .../Providers/Rng/HashRNGProviderTest.php | 24 + .../tests/Providers/Rng/IRNGProviderTest.php | 61 + .../Providers/Rng/MCryptRNGProviderTest.php | 32 + .../tests/Providers/Rng/NeedsRngLengths.php | 9 + .../Providers/Rng/OpenSSLRNGProviderTest.php | 37 + .../tests/Providers/Rng/TestRNGProvider.php | 41 + .../Providers/Time/ITimeProviderTest.php | 53 + .../tests/Providers/Time/TestTimeProvider.php | 27 + .../twofactorauth/tests/TwoFactorAuthTest.php | 246 + user/plugins/sitemap/.gitignore | 1 + user/plugins/sitemap/CHANGELOG.md | 167 + user/plugins/sitemap/LICENSE | 21 + user/plugins/sitemap/README.md | 137 + user/plugins/sitemap/blueprints.yaml | 226 + user/plugins/sitemap/blueprints/sitemap.yaml | 59 + user/plugins/sitemap/classes/SitemapEntry.php | 243 + user/plugins/sitemap/composer.json | 30 + user/plugins/sitemap/composer.lock | 23 + user/plugins/sitemap/hebe.json | 15 + user/plugins/sitemap/languages.yaml | 142 + user/plugins/sitemap/pages/sitemap.md | 3 + user/plugins/sitemap/sitemap.php | 316 + user/plugins/sitemap/sitemap.xsl | 107 + user/plugins/sitemap/sitemap.yaml | 16 + .../sitemap/templates/sitemap.html.twig | 31 + .../sitemap/templates/sitemap.json.twig | 1 + .../sitemap/templates/sitemap.xml.twig | 42 + user/plugins/sitemap/vendor/autoload.php | 7 + .../sitemap/vendor/composer/ClassLoader.php | 445 + .../vendor/composer/InstalledVersions.php | 219 + user/plugins/sitemap/vendor/composer/LICENSE | 21 + .../vendor/composer/autoload_classmap.php | 11 + .../vendor/composer/autoload_namespaces.php | 9 + .../sitemap/vendor/composer/autoload_psr4.php | 10 + .../sitemap/vendor/composer/autoload_real.php | 57 + .../vendor/composer/autoload_static.php | 37 + .../sitemap/vendor/composer/installed.json | 5 + .../sitemap/vendor/composer/installed.php | 24 + .../vendor/composer/platform_check.php | 26 + user/plugins/taxonomylist/.gitignore | 1 + user/plugins/taxonomylist/CHANGELOG.md | 93 + user/plugins/taxonomylist/LICENSE | 21 + user/plugins/taxonomylist/README.md | 64 + user/plugins/taxonomylist/assets/readme_1.png | Bin 0 -> 12888 bytes user/plugins/taxonomylist/blueprints.yaml | 39 + .../taxonomylist/classes/taxonomylist.php | 97 + user/plugins/taxonomylist/composer.json | 36 + user/plugins/taxonomylist/composer.lock | 22 + user/plugins/taxonomylist/hebe.json | 15 + user/plugins/taxonomylist/taxonomylist.php | 75 + user/plugins/taxonomylist/taxonomylist.yaml | 2 + .../templates/partials/taxonomylist.html.twig | 10 + user/plugins/taxonomylist/vendor/autoload.php | 7 + .../vendor/composer/ClassLoader.php | 445 + .../taxonomylist/vendor/composer/LICENSE | 21 + .../vendor/composer/autoload_classmap.php | 11 + .../vendor/composer/autoload_namespaces.php | 9 + .../vendor/composer/autoload_psr4.php | 9 + .../vendor/composer/autoload_real.php | 52 + .../vendor/composer/autoload_static.php | 21 + .../vendor/composer/installed.json | 1 + user/themes/.gitkeep | 0 user/themes/figureslibres-v2/CHANGELOG.md | 5 + user/themes/figureslibres-v2/LICENSE | 21 + user/themes/figureslibres-v2/README.md | 7 + user/themes/figureslibres-v2/blueprints.yaml | 32 + user/themes/figureslibres-v2/css/custom.css | 175 + .../figureslibres-v2/css/owl.carousel.min.css | 6 + user/themes/figureslibres-v2/css/theme.css | 1087 + .../themes/figureslibres-v2/css/theme.css.map | 1 + .../figureslibres-v2/figureslibres-v2.php | 9 + .../figureslibres-v2/figureslibres-v2.yaml | 3 + .../Avara-bold-italic/Avara-bold-italic.ttf | Bin 0 -> 33696 bytes .../Avara-bold-italic/Avara-bold-italic.woff | Bin 0 -> 16475 bytes .../Avara-bold-italic/Avara-bold-italic.woff2 | Bin 0 -> 12600 bytes .../Lato-bold-italic/Lato-bold-italic.ttf | Bin 0 -> 77680 bytes .../Lato-bold-italic/Lato-bold-italic.woff | Bin 0 -> 32312 bytes .../Lato-bold-italic/Lato-bold-italic.woff2 | Bin 0 -> 25460 bytes .../fonts/Lato/Lato-bold/Lato-bold.ttf | Bin 0 -> 73316 bytes .../fonts/Lato/Lato-bold/Lato-bold.woff | Bin 0 -> 30344 bytes .../fonts/Lato/Lato-bold/Lato-bold.woff2 | Bin 0 -> 24084 bytes .../fonts/Lato/Lato-italic/Lato-italic.ttf | Bin 0 -> 75744 bytes .../fonts/Lato/Lato-italic/Lato-italic.woff | Bin 0 -> 32240 bytes .../fonts/Lato/Lato-italic/Lato-italic.woff2 | Bin 0 -> 25460 bytes .../fonts/Lato/Lato-regular/Lato-regular.ttf | Bin 0 -> 75136 bytes .../fonts/Lato/Lato-regular/Lato-regular.woff | Bin 0 -> 30964 bytes .../Lato/Lato-regular/Lato-regular.woff2 | Bin 0 -> 24480 bytes .../fonts/Lato/stylesheet.css | 48 + .../ManifontGrotesk-bold-italic.ttf | Bin 0 -> 25708 bytes .../ManifontGrotesk-bold-italic.woff | Bin 0 -> 23480 bytes .../ManifontGrotesk-bold-italic.woff2 | Bin 0 -> 9508 bytes .../ManifontGrotesk-italic.ttf | Bin 0 -> 24188 bytes .../ManifontGrotesk-italic.woff | Bin 0 -> 23392 bytes .../ManifontGrotesk-italic.woff2 | Bin 0 -> 9440 bytes .../fonts/Moche/Moche-bold/Moche-bold.ttf | Bin 0 -> 24696 bytes .../fonts/Moche/Moche-bold/Moche-bold.woff | Bin 0 -> 14372 bytes .../fonts/Moche/Moche-bold/Moche-bold.woff2 | Bin 0 -> 10484 bytes .../PlayfairDisplay-italic.ttf | Bin 0 -> 60352 bytes .../PlayfairDisplay-italic.woff | Bin 0 -> 25064 bytes .../PlayfairDisplay-italic.woff2 | Bin 0 -> 19784 bytes .../Redaction50-bold/Redaction50-bold.ttf | Bin 0 -> 54872 bytes .../Redaction50-bold/Redaction50-bold.woff | Bin 0 -> 16640 bytes .../Redaction50-bold/Redaction50-bold.woff2 | Bin 0 -> 11332 bytes .../fonts/Rumeur/Rumeur-bold/Rumeur-bold.ttf | Bin 0 -> 24084 bytes .../fonts/Rumeur/Rumeur-bold/Rumeur-bold.woff | Bin 0 -> 14420 bytes .../Rumeur/Rumeur-bold/Rumeur-bold.woff2 | Bin 0 -> 11508 bytes .../fonts/Syne/Syne-bold/Syne-bold.ttf | Bin 0 -> 25312 bytes .../fonts/Syne/Syne-bold/Syne-bold.woff | Bin 0 -> 14804 bytes .../fonts/Syne/Syne-bold/Syne-bold.woff2 | Bin 0 -> 10968 bytes .../fonts/Syne/Syne-regular/Syne-regular.ttf | Bin 0 -> 25440 bytes .../fonts/Syne/Syne-regular/Syne-regular.woff | Bin 0 -> 14644 bytes .../Syne/Syne-regular/Syne-regular.woff2 | Bin 0 -> 10964 bytes .../figureslibres-v2/images/favicon.png | Bin 0 -> 6025 bytes .../figureslibres-v2/images/loader/circle.mp4 | Bin 0 -> 4873 bytes .../images/loader/rolling.svg | 6 + .../images/pictos/arrow-diagonal.svg | 65 + .../images/pictos/arrow-down.svg | 61 + .../images/pictos/arrow-left.svg | 65 + .../images/pictos/arrow-right.svg | 63 + .../figureslibres-v2/images/pictos/burger.svg | 78 + .../images/pictos/chevron-down.svg | 66 + .../figureslibres-v2/images/pictos/cross.svg | 66 + .../js/libraries/blotter/blotter.min.js | 30 + .../blotter/liquidDistortMaterial.js | 50 + .../js/libraries/gsap/gsap.min.js | 11 + .../js/libraries/lazy/jquery.lazy.min.js | 2 + .../js/libraries/owl.carousel.min.js | 7 + user/themes/figureslibres-v2/js/script.js | 890 + user/themes/figureslibres-v2/screenshot.jpg | Bin 0 -> 15428 bytes .../scss/configuration/_fonts.scss | 43 + .../scss/configuration/_reset.css | 47 + .../scss/configuration/_variable.scss | 120 + user/themes/figureslibres-v2/scss/style.scss | 1020 + user/themes/figureslibres-v2/scss/theme.scss | 5 + .../templates/default.html.twig | 5 + .../templates/error.html.twig | 8 + .../templates/index.html.twig | 36 + .../templates/partials/base.html.twig | 70 + .../templates/partials/carrousel.html.twig | 24 + .../partials/extrait-projets.html.twig | 42 + .../templates/partials/navigation.html.twig | 21 + .../templates/partials/projet_card.html.twig | 37 + .../templates/partials/relatedpages.html.twig | 50 + .../templates/projets-index.html.twig | 39 + .../templates/reader.html.twig | 61 + .../templates/text-content.html.twig | 11 + user/themes/figureslibres-v2/thumbnail.jpg | Bin 0 -> 3681 bytes webserver-configs/Caddyfile | 31 + webserver-configs/Caddyfile-0.8.x | 33 + webserver-configs/htaccess.txt | 78 + webserver-configs/lighttpd.conf | 48 + webserver-configs/nginx.conf | 44 + webserver-configs/web.config | 42 + 3397 files changed, 593718 insertions(+) create mode 100644 .dependencies create mode 100644 .editorconfig create mode 100644 .github/FUNDING.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .htaccess create mode 100644 .phan/config.php create mode 100644 .phan/internal_stubs/Redis.phan_php create mode 100644 .phan/internal_stubs/memcache.phan_php create mode 100644 .phan/internal_stubs/memcached.phan_php create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 assets/.gitkeep create mode 100644 backup/.gitkeep create mode 100644 bin/composer.phar create mode 100644 bin/gpm create mode 100644 bin/grav create mode 100644 bin/plugin create mode 100644 cache/.gitkeep create mode 100644 codeception.yml create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 fixperms.sh create mode 100644 images/.gitkeep create mode 100644 index.php create mode 100644 logs/.gitkeep create mode 100644 now.json create mode 100644 robots.txt create mode 100644 system/assets/debugger/clockwork.css create mode 100644 system/assets/debugger/clockwork.js create mode 100644 system/assets/debugger/phpdebugbar.css create mode 100644 system/assets/grav.png create mode 100644 system/assets/jquery/jquery-2.1.4.min.js create mode 100644 system/assets/jquery/jquery-2.x.min.js create mode 100644 system/assets/jquery/jquery-3.x.min.js create mode 100644 system/assets/responsive-overlays/1x.png create mode 100644 system/assets/responsive-overlays/2x.png create mode 100644 system/assets/responsive-overlays/3x.png create mode 100644 system/assets/responsive-overlays/4x.png create mode 100644 system/assets/responsive-overlays/unknown.png create mode 100644 system/assets/whoops.css create mode 100644 system/blueprints/config/backups.yaml create mode 100644 system/blueprints/config/media.yaml create mode 100644 system/blueprints/config/scheduler.yaml create mode 100644 system/blueprints/config/security.yaml create mode 100644 system/blueprints/config/site.yaml create mode 100644 system/blueprints/config/streams.yaml create mode 100644 system/blueprints/config/system.yaml create mode 100644 system/blueprints/flex/accounts.yaml create mode 100644 system/blueprints/flex/configure/compat.yaml create mode 100644 system/blueprints/flex/pages.yaml create mode 100644 system/blueprints/flex/shared/configure.yaml create mode 100644 system/blueprints/flex/user-accounts.yaml create mode 100644 system/blueprints/flex/user-groups.yaml create mode 100644 system/blueprints/pages/default.yaml create mode 100644 system/blueprints/pages/external.yaml create mode 100644 system/blueprints/pages/modular.yaml create mode 100644 system/blueprints/pages/partials/security.yaml create mode 100644 system/blueprints/pages/root.yaml create mode 100644 system/blueprints/user/account.yaml create mode 100644 system/blueprints/user/account_new.yaml create mode 100644 system/blueprints/user/group.yaml create mode 100644 system/blueprints/user/group_new.yaml create mode 100644 system/config/backups.yaml create mode 100644 system/config/media.yaml create mode 100644 system/config/mime.yaml create mode 100644 system/config/permissions.yaml create mode 100644 system/config/security.yaml create mode 100644 system/config/site.yaml create mode 100644 system/config/system.yaml create mode 100644 system/defines.php create mode 100644 system/images/media/thumb-3dm.png create mode 100644 system/images/media/thumb-3ds.png create mode 100644 system/images/media/thumb-3g2.png create mode 100644 system/images/media/thumb-3gp.png create mode 100644 system/images/media/thumb-7z.png create mode 100644 system/images/media/thumb-aac.png create mode 100644 system/images/media/thumb-ai.png create mode 100644 system/images/media/thumb-aif.png create mode 100644 system/images/media/thumb-apk.png create mode 100644 system/images/media/thumb-app.png create mode 100644 system/images/media/thumb-asf.png create mode 100644 system/images/media/thumb-asp.png create mode 100644 system/images/media/thumb-aspx.png create mode 100644 system/images/media/thumb-asx.png create mode 100644 system/images/media/thumb-avi.png create mode 100644 system/images/media/thumb-bak.png create mode 100644 system/images/media/thumb-bat.png create mode 100644 system/images/media/thumb-bin.png create mode 100644 system/images/media/thumb-bmp.png create mode 100644 system/images/media/thumb-cab.png create mode 100644 system/images/media/thumb-cad.png create mode 100644 system/images/media/thumb-cdr.png create mode 100644 system/images/media/thumb-cer.png create mode 100644 system/images/media/thumb-cfg.png create mode 100644 system/images/media/thumb-cfm.png create mode 100644 system/images/media/thumb-cgi.png create mode 100644 system/images/media/thumb-com.png create mode 100644 system/images/media/thumb-cpl.png create mode 100644 system/images/media/thumb-cpp.png create mode 100644 system/images/media/thumb-crx.png create mode 100644 system/images/media/thumb-csr.png create mode 100644 system/images/media/thumb-css.png create mode 100644 system/images/media/thumb-csv.png create mode 100644 system/images/media/thumb-cue.png create mode 100644 system/images/media/thumb-cur.png create mode 100644 system/images/media/thumb-dat.png create mode 100644 system/images/media/thumb-db.png create mode 100644 system/images/media/thumb-dbf.png create mode 100644 system/images/media/thumb-dds.png create mode 100644 system/images/media/thumb-dem.png create mode 100644 system/images/media/thumb-dll.png create mode 100644 system/images/media/thumb-dmg.png create mode 100644 system/images/media/thumb-dmp.png create mode 100644 system/images/media/thumb-doc.png create mode 100644 system/images/media/thumb-docx.png create mode 100644 system/images/media/thumb-drv.png create mode 100644 system/images/media/thumb-dtd.png create mode 100644 system/images/media/thumb-dwg.png create mode 100644 system/images/media/thumb-dxf.png create mode 100644 system/images/media/thumb-elf.png create mode 100644 system/images/media/thumb-eot.png create mode 100644 system/images/media/thumb-eps.png create mode 100644 system/images/media/thumb-exe.png create mode 100644 system/images/media/thumb-fla.png create mode 100644 system/images/media/thumb-flv.png create mode 100644 system/images/media/thumb-fnt.png create mode 100644 system/images/media/thumb-fon.png create mode 100644 system/images/media/thumb-gam.png create mode 100644 system/images/media/thumb-gbr.png create mode 100644 system/images/media/thumb-ged.png create mode 100644 system/images/media/thumb-gif.png create mode 100644 system/images/media/thumb-gpx.png create mode 100644 system/images/media/thumb-gz.png create mode 100644 system/images/media/thumb-gzip.png create mode 100644 system/images/media/thumb-hqz.png create mode 100644 system/images/media/thumb-html.png create mode 100644 system/images/media/thumb-icns.png create mode 100644 system/images/media/thumb-ico.png create mode 100644 system/images/media/thumb-ics.png create mode 100644 system/images/media/thumb-iff.png create mode 100644 system/images/media/thumb-indd.png create mode 100644 system/images/media/thumb-iso.png create mode 100644 system/images/media/thumb-jar.png create mode 100644 system/images/media/thumb-jpg.png create mode 100644 system/images/media/thumb-js.png create mode 100644 system/images/media/thumb-json.png create mode 100644 system/images/media/thumb-jsp.png create mode 100644 system/images/media/thumb-key.png create mode 100644 system/images/media/thumb-kml.png create mode 100644 system/images/media/thumb-kmz.png create mode 100644 system/images/media/thumb-lnk.png create mode 100644 system/images/media/thumb-log.png create mode 100644 system/images/media/thumb-lua.png create mode 100644 system/images/media/thumb-m3u.png create mode 100644 system/images/media/thumb-m4a.png create mode 100644 system/images/media/thumb-m4v.png create mode 100644 system/images/media/thumb-max.png create mode 100644 system/images/media/thumb-mdb.png create mode 100644 system/images/media/thumb-mdf.png create mode 100644 system/images/media/thumb-mid.png create mode 100644 system/images/media/thumb-mim.png create mode 100644 system/images/media/thumb-mov.png create mode 100644 system/images/media/thumb-mp3.png create mode 100644 system/images/media/thumb-mp4.png create mode 100644 system/images/media/thumb-mpa.png create mode 100644 system/images/media/thumb-mpe.png create mode 100644 system/images/media/thumb-mpg.png create mode 100644 system/images/media/thumb-msg.png create mode 100644 system/images/media/thumb-msi.png create mode 100644 system/images/media/thumb-nes.png create mode 100644 system/images/media/thumb-obj.png create mode 100644 system/images/media/thumb-odb.png create mode 100644 system/images/media/thumb-odc.png create mode 100644 system/images/media/thumb-odf.png create mode 100644 system/images/media/thumb-odg.png create mode 100644 system/images/media/thumb-odi.png create mode 100644 system/images/media/thumb-odp.png create mode 100644 system/images/media/thumb-ods.png create mode 100644 system/images/media/thumb-odt.png create mode 100644 system/images/media/thumb-odx.png create mode 100644 system/images/media/thumb-ogg.png create mode 100644 system/images/media/thumb-pct.png create mode 100644 system/images/media/thumb-pdb.png create mode 100644 system/images/media/thumb-pdf.png create mode 100644 system/images/media/thumb-pif.png create mode 100644 system/images/media/thumb-pkg.png create mode 100644 system/images/media/thumb-pl.png create mode 100644 system/images/media/thumb-png.png create mode 100644 system/images/media/thumb-pps.png create mode 100644 system/images/media/thumb-ppt.png create mode 100644 system/images/media/thumb-pptx.png create mode 100644 system/images/media/thumb-ps.png create mode 100644 system/images/media/thumb-psd.png create mode 100644 system/images/media/thumb-pub.png create mode 100644 system/images/media/thumb-py.png create mode 100644 system/images/media/thumb-ra.png create mode 100644 system/images/media/thumb-rar.png create mode 100644 system/images/media/thumb-raw.png create mode 100644 system/images/media/thumb-rm.png create mode 100644 system/images/media/thumb-rom.png create mode 100644 system/images/media/thumb-rpm.png create mode 100644 system/images/media/thumb-rss.png create mode 100644 system/images/media/thumb-rtf.png create mode 100644 system/images/media/thumb-sav.png create mode 100644 system/images/media/thumb-sdf.png create mode 100644 system/images/media/thumb-sql.png create mode 100644 system/images/media/thumb-srt.png create mode 100644 system/images/media/thumb-svg.png create mode 100644 system/images/media/thumb-swf.png create mode 100644 system/images/media/thumb-sys.png create mode 100644 system/images/media/thumb-tar.png create mode 100644 system/images/media/thumb-tex.png create mode 100644 system/images/media/thumb-tga.png create mode 100644 system/images/media/thumb-thm.png create mode 100644 system/images/media/thumb-tiff.png create mode 100644 system/images/media/thumb-tmp.png create mode 100644 system/images/media/thumb-ttf.png create mode 100644 system/images/media/thumb-txt.png create mode 100644 system/images/media/thumb-uue.png create mode 100644 system/images/media/thumb-vb.png create mode 100644 system/images/media/thumb-vcd.png create mode 100644 system/images/media/thumb-vcf.png create mode 100644 system/images/media/thumb-wav.png create mode 100644 system/images/media/thumb-webm.png create mode 100644 system/images/media/thumb-wma.png create mode 100644 system/images/media/thumb-wmv.png create mode 100644 system/images/media/thumb-woff.png create mode 100644 system/images/media/thumb-woff2.png create mode 100644 system/images/media/thumb-wpd.png create mode 100644 system/images/media/thumb-wps.png create mode 100644 system/images/media/thumb-wsf.png create mode 100644 system/images/media/thumb-xls.png create mode 100644 system/images/media/thumb-xlsx.png create mode 100644 system/images/media/thumb-xml.png create mode 100644 system/images/media/thumb-yuv.png create mode 100644 system/images/media/thumb-zip.png create mode 100644 system/images/media/thumb.png create mode 100644 system/images/watermark.png create mode 100644 system/install.php create mode 100644 system/languages/ar.yaml create mode 100644 system/languages/bg.yaml create mode 100644 system/languages/ca.yaml create mode 100644 system/languages/cs.yaml create mode 100644 system/languages/da.yaml create mode 100644 system/languages/de.yaml create mode 100644 system/languages/el.yaml create mode 100644 system/languages/en.yaml create mode 100644 system/languages/es.yaml create mode 100644 system/languages/et.yaml create mode 100644 system/languages/eu.yaml create mode 100644 system/languages/fa.yaml create mode 100644 system/languages/fi.yaml create mode 100644 system/languages/fr.yaml create mode 100644 system/languages/gl.yaml create mode 100644 system/languages/he.yaml create mode 100644 system/languages/hr.yaml create mode 100644 system/languages/hu.yaml create mode 100644 system/languages/id.yaml create mode 100644 system/languages/is.yaml create mode 100644 system/languages/it.yaml create mode 100644 system/languages/ja.yaml create mode 100644 system/languages/ko.yaml create mode 100644 system/languages/lt.yaml create mode 100644 system/languages/mn.yaml create mode 100644 system/languages/nb.yaml create mode 100644 system/languages/nl.yaml create mode 100644 system/languages/no.yaml create mode 100644 system/languages/pl.yaml create mode 100644 system/languages/pt.yaml create mode 100644 system/languages/ro.yaml create mode 100644 system/languages/ru.yaml create mode 100644 system/languages/si.yaml create mode 100644 system/languages/sk.yaml create mode 100644 system/languages/sl.yaml create mode 100644 system/languages/sr.yaml create mode 100644 system/languages/sv.yaml create mode 100644 system/languages/th.yaml create mode 100644 system/languages/tr.yaml create mode 100644 system/languages/uk.yaml create mode 100644 system/languages/vi.yaml create mode 100644 system/languages/zh-cn.yaml create mode 100644 system/languages/zh-tw.yaml create mode 100644 system/languages/zh.yaml create mode 100644 system/pages/notfound.md create mode 100644 system/router.php create mode 100644 system/src/Grav/Common/Assets.php create mode 100644 system/src/Grav/Common/Assets/BaseAsset.php create mode 100644 system/src/Grav/Common/Assets/Css.php create mode 100644 system/src/Grav/Common/Assets/InlineCss.php create mode 100644 system/src/Grav/Common/Assets/InlineJs.php create mode 100644 system/src/Grav/Common/Assets/Js.php create mode 100644 system/src/Grav/Common/Assets/Pipeline.php create mode 100644 system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php create mode 100644 system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php create mode 100644 system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php create mode 100644 system/src/Grav/Common/Backup/Backups.php create mode 100644 system/src/Grav/Common/Browser.php create mode 100644 system/src/Grav/Common/Cache.php create mode 100644 system/src/Grav/Common/Composer.php create mode 100644 system/src/Grav/Common/Config/CompiledBase.php create mode 100644 system/src/Grav/Common/Config/CompiledBlueprints.php create mode 100644 system/src/Grav/Common/Config/CompiledConfig.php create mode 100644 system/src/Grav/Common/Config/CompiledLanguages.php create mode 100644 system/src/Grav/Common/Config/Config.php create mode 100644 system/src/Grav/Common/Config/ConfigFileFinder.php create mode 100644 system/src/Grav/Common/Config/Languages.php create mode 100644 system/src/Grav/Common/Config/Setup.php create mode 100644 system/src/Grav/Common/Data/Blueprint.php create mode 100644 system/src/Grav/Common/Data/BlueprintSchema.php create mode 100644 system/src/Grav/Common/Data/Blueprints.php create mode 100644 system/src/Grav/Common/Data/Data.php create mode 100644 system/src/Grav/Common/Data/DataInterface.php create mode 100644 system/src/Grav/Common/Data/Validation.php create mode 100644 system/src/Grav/Common/Data/ValidationException.php create mode 100644 system/src/Grav/Common/Debugger.php create mode 100644 system/src/Grav/Common/Errors/BareHandler.php create mode 100644 system/src/Grav/Common/Errors/Errors.php create mode 100644 system/src/Grav/Common/Errors/Resources/error.css create mode 100644 system/src/Grav/Common/Errors/Resources/layout.html.php create mode 100644 system/src/Grav/Common/Errors/SimplePageHandler.php create mode 100644 system/src/Grav/Common/Errors/SystemFacade.php create mode 100644 system/src/Grav/Common/File/CompiledFile.php create mode 100644 system/src/Grav/Common/File/CompiledJsonFile.php create mode 100644 system/src/Grav/Common/File/CompiledMarkdownFile.php create mode 100644 system/src/Grav/Common/File/CompiledYamlFile.php create mode 100644 system/src/Grav/Common/Filesystem/Archiver.php create mode 100644 system/src/Grav/Common/Filesystem/Folder.php create mode 100644 system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php create mode 100644 system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php create mode 100644 system/src/Grav/Common/Filesystem/ZipArchiver.php create mode 100644 system/src/Grav/Common/Flex/FlexCollection.php create mode 100644 system/src/Grav/Common/Flex/FlexIndex.php create mode 100644 system/src/Grav/Common/Flex/FlexObject.php create mode 100644 system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php create mode 100644 system/src/Grav/Common/Flex/Traits/FlexCommonTrait.php create mode 100644 system/src/Grav/Common/Flex/Traits/FlexGravTrait.php create mode 100644 system/src/Grav/Common/Flex/Traits/FlexIndexTrait.php create mode 100644 system/src/Grav/Common/Flex/Traits/FlexObjectTrait.php create mode 100644 system/src/Grav/Common/Flex/Types/Generic/GenericCollection.php create mode 100644 system/src/Grav/Common/Flex/Types/Generic/GenericIndex.php create mode 100644 system/src/Grav/Common/Flex/Types/Generic/GenericObject.php create mode 100644 system/src/Grav/Common/Flex/Types/Pages/PageCollection.php create mode 100644 system/src/Grav/Common/Flex/Types/Pages/PageIndex.php create mode 100644 system/src/Grav/Common/Flex/Types/Pages/PageObject.php create mode 100644 system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php create mode 100644 system/src/Grav/Common/Flex/Types/Pages/Traits/PageContentTrait.php create mode 100644 system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.php create mode 100644 system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php create mode 100644 system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php create mode 100644 system/src/Grav/Common/Flex/Types/UserGroups/UserGroupCollection.php create mode 100644 system/src/Grav/Common/Flex/Types/UserGroups/UserGroupIndex.php create mode 100644 system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php create mode 100644 system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php create mode 100644 system/src/Grav/Common/Flex/Types/Users/Storage/UserFolderStorage.php create mode 100644 system/src/Grav/Common/Flex/Types/Users/Traits/UserObjectLegacyTrait.php create mode 100644 system/src/Grav/Common/Flex/Types/Users/UserCollection.php create mode 100644 system/src/Grav/Common/Flex/Types/Users/UserIndex.php create mode 100644 system/src/Grav/Common/Flex/Types/Users/UserObject.php create mode 100644 system/src/Grav/Common/Form/FormFlash.php create mode 100644 system/src/Grav/Common/GPM/AbstractCollection.php create mode 100644 system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php create mode 100644 system/src/Grav/Common/GPM/Common/CachedCollection.php create mode 100644 system/src/Grav/Common/GPM/Common/Package.php create mode 100644 system/src/Grav/Common/GPM/GPM.php create mode 100644 system/src/Grav/Common/GPM/Installer.php create mode 100644 system/src/Grav/Common/GPM/Licenses.php create mode 100644 system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php create mode 100644 system/src/Grav/Common/GPM/Local/Package.php create mode 100644 system/src/Grav/Common/GPM/Local/Packages.php create mode 100644 system/src/Grav/Common/GPM/Local/Plugins.php create mode 100644 system/src/Grav/Common/GPM/Local/Themes.php create mode 100644 system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php create mode 100644 system/src/Grav/Common/GPM/Remote/GravCore.php create mode 100644 system/src/Grav/Common/GPM/Remote/Package.php create mode 100644 system/src/Grav/Common/GPM/Remote/Packages.php create mode 100644 system/src/Grav/Common/GPM/Remote/Plugins.php create mode 100644 system/src/Grav/Common/GPM/Remote/Themes.php create mode 100644 system/src/Grav/Common/GPM/Response.php create mode 100644 system/src/Grav/Common/GPM/Upgrader.php create mode 100644 system/src/Grav/Common/Getters.php create mode 100644 system/src/Grav/Common/Grav.php create mode 100644 system/src/Grav/Common/GravTrait.php create mode 100644 system/src/Grav/Common/HTTP/Client.php create mode 100644 system/src/Grav/Common/HTTP/Response.php create mode 100644 system/src/Grav/Common/Helpers/Base32.php create mode 100644 system/src/Grav/Common/Helpers/Excerpts.php create mode 100644 system/src/Grav/Common/Helpers/Exif.php create mode 100644 system/src/Grav/Common/Helpers/LogViewer.php create mode 100644 system/src/Grav/Common/Helpers/Truncator.php create mode 100644 system/src/Grav/Common/Helpers/YamlLinter.php create mode 100644 system/src/Grav/Common/Inflector.php create mode 100644 system/src/Grav/Common/Iterator.php create mode 100644 system/src/Grav/Common/Language/Language.php create mode 100644 system/src/Grav/Common/Language/LanguageCodes.php create mode 100644 system/src/Grav/Common/Markdown/Parsedown.php create mode 100644 system/src/Grav/Common/Markdown/ParsedownExtra.php create mode 100644 system/src/Grav/Common/Markdown/ParsedownGravTrait.php create mode 100644 system/src/Grav/Common/Media/Interfaces/AudioMediaInterface.php create mode 100644 system/src/Grav/Common/Media/Interfaces/ImageManipulateInterface.php create mode 100644 system/src/Grav/Common/Media/Interfaces/ImageMediaInterface.php create mode 100644 system/src/Grav/Common/Media/Interfaces/MediaCollectionInterface.php create mode 100644 system/src/Grav/Common/Media/Interfaces/MediaFileInterface.php create mode 100644 system/src/Grav/Common/Media/Interfaces/MediaInterface.php create mode 100644 system/src/Grav/Common/Media/Interfaces/MediaLinkInterface.php create mode 100644 system/src/Grav/Common/Media/Interfaces/MediaObjectInterface.php create mode 100644 system/src/Grav/Common/Media/Interfaces/MediaPlayerInterface.php create mode 100644 system/src/Grav/Common/Media/Interfaces/MediaUploadInterface.php create mode 100644 system/src/Grav/Common/Media/Interfaces/VideoMediaInterface.php create mode 100644 system/src/Grav/Common/Media/Traits/AudioMediaTrait.php create mode 100644 system/src/Grav/Common/Media/Traits/ImageLoadingTrait.php create mode 100644 system/src/Grav/Common/Media/Traits/ImageMediaTrait.php create mode 100644 system/src/Grav/Common/Media/Traits/MediaFileTrait.php create mode 100644 system/src/Grav/Common/Media/Traits/MediaObjectTrait.php create mode 100644 system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php create mode 100644 system/src/Grav/Common/Media/Traits/MediaTrait.php create mode 100644 system/src/Grav/Common/Media/Traits/MediaUploadTrait.php create mode 100644 system/src/Grav/Common/Media/Traits/StaticResizeTrait.php create mode 100644 system/src/Grav/Common/Media/Traits/ThumbnailMediaTrait.php create mode 100644 system/src/Grav/Common/Media/Traits/VideoMediaTrait.php create mode 100644 system/src/Grav/Common/Page/Collection.php create mode 100644 system/src/Grav/Common/Page/Header.php create mode 100644 system/src/Grav/Common/Page/Interfaces/PageCollectionInterface.php create mode 100644 system/src/Grav/Common/Page/Interfaces/PageContentInterface.php create mode 100644 system/src/Grav/Common/Page/Interfaces/PageFormInterface.php create mode 100644 system/src/Grav/Common/Page/Interfaces/PageInterface.php create mode 100644 system/src/Grav/Common/Page/Interfaces/PageLegacyInterface.php create mode 100644 system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php create mode 100644 system/src/Grav/Common/Page/Interfaces/PageTranslateInterface.php create mode 100644 system/src/Grav/Common/Page/Interfaces/PagesSourceInterface.php create mode 100644 system/src/Grav/Common/Page/Markdown/Excerpts.php create mode 100644 system/src/Grav/Common/Page/Media.php create mode 100644 system/src/Grav/Common/Page/Medium/AbstractMedia.php create mode 100644 system/src/Grav/Common/Page/Medium/AudioMedium.php create mode 100644 system/src/Grav/Common/Page/Medium/GlobalMedia.php create mode 100644 system/src/Grav/Common/Page/Medium/ImageFile.php create mode 100644 system/src/Grav/Common/Page/Medium/ImageMedium.php create mode 100644 system/src/Grav/Common/Page/Medium/Link.php create mode 100644 system/src/Grav/Common/Page/Medium/Medium.php create mode 100644 system/src/Grav/Common/Page/Medium/MediumFactory.php create mode 100644 system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php create mode 100644 system/src/Grav/Common/Page/Medium/RenderableInterface.php create mode 100644 system/src/Grav/Common/Page/Medium/StaticImageMedium.php create mode 100644 system/src/Grav/Common/Page/Medium/StaticResizeTrait.php create mode 100644 system/src/Grav/Common/Page/Medium/ThumbnailImageMedium.php create mode 100644 system/src/Grav/Common/Page/Medium/VideoMedium.php create mode 100644 system/src/Grav/Common/Page/Page.php create mode 100644 system/src/Grav/Common/Page/Pages.php create mode 100644 system/src/Grav/Common/Page/Traits/PageFormTrait.php create mode 100644 system/src/Grav/Common/Page/Types.php create mode 100644 system/src/Grav/Common/Plugin.php create mode 100644 system/src/Grav/Common/Plugins.php create mode 100644 system/src/Grav/Common/Processors/AssetsProcessor.php create mode 100644 system/src/Grav/Common/Processors/BackupsProcessor.php create mode 100644 system/src/Grav/Common/Processors/DebuggerAssetsProcessor.php create mode 100644 system/src/Grav/Common/Processors/Events/RequestHandlerEvent.php create mode 100644 system/src/Grav/Common/Processors/InitializeProcessor.php create mode 100644 system/src/Grav/Common/Processors/PagesProcessor.php create mode 100644 system/src/Grav/Common/Processors/PluginsProcessor.php create mode 100644 system/src/Grav/Common/Processors/ProcessorBase.php create mode 100644 system/src/Grav/Common/Processors/ProcessorInterface.php create mode 100644 system/src/Grav/Common/Processors/RenderProcessor.php create mode 100644 system/src/Grav/Common/Processors/RequestProcessor.php create mode 100644 system/src/Grav/Common/Processors/SchedulerProcessor.php create mode 100644 system/src/Grav/Common/Processors/TasksProcessor.php create mode 100644 system/src/Grav/Common/Processors/ThemesProcessor.php create mode 100644 system/src/Grav/Common/Processors/TwigProcessor.php create mode 100644 system/src/Grav/Common/Scheduler/Cron.php create mode 100644 system/src/Grav/Common/Scheduler/IntervalTrait.php create mode 100644 system/src/Grav/Common/Scheduler/Job.php create mode 100644 system/src/Grav/Common/Scheduler/Scheduler.php create mode 100644 system/src/Grav/Common/Security.php create mode 100644 system/src/Grav/Common/Service/AccountsServiceProvider.php create mode 100644 system/src/Grav/Common/Service/AssetsServiceProvider.php create mode 100644 system/src/Grav/Common/Service/BackupsServiceProvider.php create mode 100644 system/src/Grav/Common/Service/ConfigServiceProvider.php create mode 100644 system/src/Grav/Common/Service/ErrorServiceProvider.php create mode 100644 system/src/Grav/Common/Service/FilesystemServiceProvider.php create mode 100644 system/src/Grav/Common/Service/FlexServiceProvider.php create mode 100644 system/src/Grav/Common/Service/InflectorServiceProvider.php create mode 100644 system/src/Grav/Common/Service/LoggerServiceProvider.php create mode 100644 system/src/Grav/Common/Service/OutputServiceProvider.php create mode 100644 system/src/Grav/Common/Service/PagesServiceProvider.php create mode 100644 system/src/Grav/Common/Service/RequestServiceProvider.php create mode 100644 system/src/Grav/Common/Service/SchedulerServiceProvider.php create mode 100644 system/src/Grav/Common/Service/SessionServiceProvider.php create mode 100644 system/src/Grav/Common/Service/StreamsServiceProvider.php create mode 100644 system/src/Grav/Common/Service/TaskServiceProvider.php create mode 100644 system/src/Grav/Common/Session.php create mode 100644 system/src/Grav/Common/Taxonomy.php create mode 100644 system/src/Grav/Common/Theme.php create mode 100644 system/src/Grav/Common/Themes.php create mode 100644 system/src/Grav/Common/Twig/Exception/TwigException.php create mode 100644 system/src/Grav/Common/Twig/Extension/FilesystemExtension.php create mode 100644 system/src/Grav/Common/Twig/Extension/GravExtension.php create mode 100644 system/src/Grav/Common/Twig/Node/TwigNodeCache.php create mode 100644 system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php create mode 100644 system/src/Grav/Common/Twig/Node/TwigNodeRender.php create mode 100644 system/src/Grav/Common/Twig/Node/TwigNodeScript.php create mode 100644 system/src/Grav/Common/Twig/Node/TwigNodeStyle.php create mode 100644 system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php create mode 100644 system/src/Grav/Common/Twig/Node/TwigNodeThrow.php create mode 100644 system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php create mode 100644 system/src/Grav/Common/Twig/TokenParser/TwigTokenParserCache.php create mode 100644 system/src/Grav/Common/Twig/TokenParser/TwigTokenParserMarkdown.php create mode 100644 system/src/Grav/Common/Twig/TokenParser/TwigTokenParserRender.php create mode 100644 system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php create mode 100644 system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php create mode 100644 system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php create mode 100644 system/src/Grav/Common/Twig/TokenParser/TwigTokenParserThrow.php create mode 100644 system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php create mode 100644 system/src/Grav/Common/Twig/Twig.php create mode 100644 system/src/Grav/Common/Twig/TwigClockworkDataSource.php create mode 100644 system/src/Grav/Common/Twig/TwigClockworkDumper.php create mode 100644 system/src/Grav/Common/Twig/TwigEnvironment.php create mode 100644 system/src/Grav/Common/Twig/TwigExtension.php create mode 100644 system/src/Grav/Common/Twig/WriteCacheFileTrait.php create mode 100644 system/src/Grav/Common/Uri.php create mode 100644 system/src/Grav/Common/User/Access.php create mode 100644 system/src/Grav/Common/User/Authentication.php create mode 100644 system/src/Grav/Common/User/DataUser/User.php create mode 100644 system/src/Grav/Common/User/DataUser/UserCollection.php create mode 100644 system/src/Grav/Common/User/Group.php create mode 100644 system/src/Grav/Common/User/Interfaces/AuthorizeInterface.php create mode 100644 system/src/Grav/Common/User/Interfaces/UserCollectionInterface.php create mode 100644 system/src/Grav/Common/User/Interfaces/UserGroupInterface.php create mode 100644 system/src/Grav/Common/User/Interfaces/UserInterface.php create mode 100644 system/src/Grav/Common/User/Traits/UserTrait.php create mode 100644 system/src/Grav/Common/User/User.php create mode 100644 system/src/Grav/Common/Utils.php create mode 100644 system/src/Grav/Common/Yaml.php create mode 100644 system/src/Grav/Console/Application/Application.php create mode 100644 system/src/Grav/Console/Application/CommandLoader/PluginCommandLoader.php create mode 100644 system/src/Grav/Console/Application/GpmApplication.php create mode 100644 system/src/Grav/Console/Application/GravApplication.php create mode 100644 system/src/Grav/Console/Application/PluginApplication.php create mode 100644 system/src/Grav/Console/Cli/BackupCommand.php create mode 100644 system/src/Grav/Console/Cli/CleanCommand.php create mode 100644 system/src/Grav/Console/Cli/ClearCacheCommand.php create mode 100644 system/src/Grav/Console/Cli/ComposerCommand.php create mode 100644 system/src/Grav/Console/Cli/InstallCommand.php create mode 100644 system/src/Grav/Console/Cli/LogViewerCommand.php create mode 100644 system/src/Grav/Console/Cli/NewProjectCommand.php create mode 100644 system/src/Grav/Console/Cli/PageSystemValidatorCommand.php create mode 100644 system/src/Grav/Console/Cli/SandboxCommand.php create mode 100644 system/src/Grav/Console/Cli/SchedulerCommand.php create mode 100644 system/src/Grav/Console/Cli/SecurityCommand.php create mode 100644 system/src/Grav/Console/Cli/ServerCommand.php create mode 100644 system/src/Grav/Console/Cli/YamlLinterCommand.php create mode 100644 system/src/Grav/Console/ConsoleCommand.php create mode 100644 system/src/Grav/Console/ConsoleTrait.php create mode 100644 system/src/Grav/Console/Gpm/DirectInstallCommand.php create mode 100644 system/src/Grav/Console/Gpm/IndexCommand.php create mode 100644 system/src/Grav/Console/Gpm/InfoCommand.php create mode 100644 system/src/Grav/Console/Gpm/InstallCommand.php create mode 100644 system/src/Grav/Console/Gpm/SelfupgradeCommand.php create mode 100644 system/src/Grav/Console/Gpm/UninstallCommand.php create mode 100644 system/src/Grav/Console/Gpm/UpdateCommand.php create mode 100644 system/src/Grav/Console/Gpm/VersionCommand.php create mode 100644 system/src/Grav/Console/GpmCommand.php create mode 100644 system/src/Grav/Console/GravCommand.php create mode 100644 system/src/Grav/Console/Plugin/PluginListCommand.php create mode 100644 system/src/Grav/Console/TerminalObjects/Table.php create mode 100644 system/src/Grav/Events/FlexRegisterEvent.php create mode 100644 system/src/Grav/Events/PermissionsRegisterEvent.php create mode 100644 system/src/Grav/Events/PluginsLoadedEvent.php create mode 100644 system/src/Grav/Events/SessionStartEvent.php create mode 100644 system/src/Grav/Framework/Acl/Access.php create mode 100644 system/src/Grav/Framework/Acl/Action.php create mode 100644 system/src/Grav/Framework/Acl/Permissions.php create mode 100644 system/src/Grav/Framework/Acl/PermissionsReader.php create mode 100644 system/src/Grav/Framework/Acl/RecursiveActionIterator.php create mode 100644 system/src/Grav/Framework/Cache/AbstractCache.php create mode 100644 system/src/Grav/Framework/Cache/Adapter/ChainCache.php create mode 100644 system/src/Grav/Framework/Cache/Adapter/DoctrineCache.php create mode 100644 system/src/Grav/Framework/Cache/Adapter/FileCache.php create mode 100644 system/src/Grav/Framework/Cache/Adapter/MemoryCache.php create mode 100644 system/src/Grav/Framework/Cache/Adapter/SessionCache.php create mode 100644 system/src/Grav/Framework/Cache/CacheInterface.php create mode 100644 system/src/Grav/Framework/Cache/CacheTrait.php create mode 100644 system/src/Grav/Framework/Cache/Exception/CacheException.php create mode 100644 system/src/Grav/Framework/Cache/Exception/InvalidArgumentException.php create mode 100644 system/src/Grav/Framework/Collection/AbstractFileCollection.php create mode 100644 system/src/Grav/Framework/Collection/AbstractIndexCollection.php create mode 100644 system/src/Grav/Framework/Collection/AbstractLazyCollection.php create mode 100644 system/src/Grav/Framework/Collection/ArrayCollection.php create mode 100644 system/src/Grav/Framework/Collection/CollectionInterface.php create mode 100644 system/src/Grav/Framework/Collection/FileCollection.php create mode 100644 system/src/Grav/Framework/Collection/FileCollectionInterface.php create mode 100644 system/src/Grav/Framework/Compat/Serializable.php create mode 100644 system/src/Grav/Framework/ContentBlock/ContentBlock.php create mode 100644 system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php create mode 100644 system/src/Grav/Framework/ContentBlock/HtmlBlock.php create mode 100644 system/src/Grav/Framework/ContentBlock/HtmlBlockInterface.php create mode 100644 system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php create mode 100644 system/src/Grav/Framework/DI/Container.php create mode 100644 system/src/Grav/Framework/File/AbstractFile.php create mode 100644 system/src/Grav/Framework/File/CsvFile.php create mode 100644 system/src/Grav/Framework/File/DataFile.php create mode 100644 system/src/Grav/Framework/File/File.php create mode 100644 system/src/Grav/Framework/File/Formatter/AbstractFormatter.php create mode 100644 system/src/Grav/Framework/File/Formatter/CsvFormatter.php create mode 100644 system/src/Grav/Framework/File/Formatter/FormatterInterface.php create mode 100644 system/src/Grav/Framework/File/Formatter/IniFormatter.php create mode 100644 system/src/Grav/Framework/File/Formatter/JsonFormatter.php create mode 100644 system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php create mode 100644 system/src/Grav/Framework/File/Formatter/SerializeFormatter.php create mode 100644 system/src/Grav/Framework/File/Formatter/YamlFormatter.php create mode 100644 system/src/Grav/Framework/File/IniFile.php create mode 100644 system/src/Grav/Framework/File/Interfaces/FileFormatterInterface.php create mode 100644 system/src/Grav/Framework/File/Interfaces/FileInterface.php create mode 100644 system/src/Grav/Framework/File/JsonFile.php create mode 100644 system/src/Grav/Framework/File/MarkdownFile.php create mode 100644 system/src/Grav/Framework/File/YamlFile.php create mode 100644 system/src/Grav/Framework/Filesystem/Filesystem.php create mode 100644 system/src/Grav/Framework/Filesystem/Interfaces/FilesystemInterface.php create mode 100644 system/src/Grav/Framework/Flex/Flex.php create mode 100644 system/src/Grav/Framework/Flex/FlexCollection.php create mode 100644 system/src/Grav/Framework/Flex/FlexDirectory.php create mode 100644 system/src/Grav/Framework/Flex/FlexDirectoryForm.php create mode 100644 system/src/Grav/Framework/Flex/FlexForm.php create mode 100644 system/src/Grav/Framework/Flex/FlexFormFlash.php create mode 100644 system/src/Grav/Framework/Flex/FlexIndex.php create mode 100644 system/src/Grav/Framework/Flex/FlexObject.php create mode 100644 system/src/Grav/Framework/Flex/Interfaces/FlexAuthorizeInterface.php create mode 100644 system/src/Grav/Framework/Flex/Interfaces/FlexCollectionInterface.php create mode 100644 system/src/Grav/Framework/Flex/Interfaces/FlexCommonInterface.php create mode 100644 system/src/Grav/Framework/Flex/Interfaces/FlexDirectoryFormInterface.php create mode 100644 system/src/Grav/Framework/Flex/Interfaces/FlexDirectoryInterface.php create mode 100644 system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php create mode 100644 system/src/Grav/Framework/Flex/Interfaces/FlexIndexInterface.php create mode 100644 system/src/Grav/Framework/Flex/Interfaces/FlexInterface.php create mode 100644 system/src/Grav/Framework/Flex/Interfaces/FlexObjectFormInterface.php create mode 100644 system/src/Grav/Framework/Flex/Interfaces/FlexObjectInterface.php create mode 100644 system/src/Grav/Framework/Flex/Interfaces/FlexStorageInterface.php create mode 100644 system/src/Grav/Framework/Flex/Interfaces/FlexTranslateInterface.php create mode 100644 system/src/Grav/Framework/Flex/Pages/FlexPageCollection.php create mode 100644 system/src/Grav/Framework/Flex/Pages/FlexPageIndex.php create mode 100644 system/src/Grav/Framework/Flex/Pages/FlexPageObject.php create mode 100644 system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php create mode 100644 system/src/Grav/Framework/Flex/Pages/Traits/PageContentTrait.php create mode 100644 system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php create mode 100644 system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php create mode 100644 system/src/Grav/Framework/Flex/Pages/Traits/PageTranslateTrait.php create mode 100644 system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php create mode 100644 system/src/Grav/Framework/Flex/Storage/FileStorage.php create mode 100644 system/src/Grav/Framework/Flex/Storage/FolderStorage.php create mode 100644 system/src/Grav/Framework/Flex/Storage/SimpleStorage.php create mode 100644 system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php create mode 100644 system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php create mode 100644 system/src/Grav/Framework/Flex/Traits/FlexRelatedDirectoryTrait.php create mode 100644 system/src/Grav/Framework/Form/FormFlash.php create mode 100644 system/src/Grav/Framework/Form/FormFlashFile.php create mode 100644 system/src/Grav/Framework/Form/Interfaces/FormFactoryInterface.php create mode 100644 system/src/Grav/Framework/Form/Interfaces/FormFlashInterface.php create mode 100644 system/src/Grav/Framework/Form/Interfaces/FormInterface.php create mode 100644 system/src/Grav/Framework/Form/Traits/FormTrait.php create mode 100644 system/src/Grav/Framework/Interfaces/RenderInterface.php create mode 100644 system/src/Grav/Framework/Logger/Processors/UserProcessor.php create mode 100644 system/src/Grav/Framework/Media/Interfaces/MediaCollectionInterface.php create mode 100644 system/src/Grav/Framework/Media/Interfaces/MediaInterface.php create mode 100644 system/src/Grav/Framework/Media/Interfaces/MediaManipulationInterface.php create mode 100644 system/src/Grav/Framework/Media/Interfaces/MediaObjectInterface.php create mode 100644 system/src/Grav/Framework/Mime/MimeTypes.php create mode 100644 system/src/Grav/Framework/Object/Access/ArrayAccessTrait.php create mode 100644 system/src/Grav/Framework/Object/Access/NestedArrayAccessTrait.php create mode 100644 system/src/Grav/Framework/Object/Access/NestedPropertyCollectionTrait.php create mode 100644 system/src/Grav/Framework/Object/Access/NestedPropertyTrait.php create mode 100644 system/src/Grav/Framework/Object/Access/OverloadedPropertyTrait.php create mode 100644 system/src/Grav/Framework/Object/ArrayObject.php create mode 100644 system/src/Grav/Framework/Object/Base/ObjectCollectionTrait.php create mode 100644 system/src/Grav/Framework/Object/Base/ObjectTrait.php create mode 100644 system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php create mode 100644 system/src/Grav/Framework/Object/Interfaces/NestedObjectCollectionInterface.php create mode 100644 system/src/Grav/Framework/Object/Interfaces/NestedObjectInterface.php create mode 100644 system/src/Grav/Framework/Object/Interfaces/ObjectCollectionInterface.php create mode 100644 system/src/Grav/Framework/Object/Interfaces/ObjectInterface.php create mode 100644 system/src/Grav/Framework/Object/LazyObject.php create mode 100644 system/src/Grav/Framework/Object/ObjectCollection.php create mode 100644 system/src/Grav/Framework/Object/ObjectIndex.php create mode 100644 system/src/Grav/Framework/Object/Property/ArrayPropertyTrait.php create mode 100644 system/src/Grav/Framework/Object/Property/LazyPropertyTrait.php create mode 100644 system/src/Grav/Framework/Object/Property/MixedPropertyTrait.php create mode 100644 system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php create mode 100644 system/src/Grav/Framework/Object/PropertyObject.php create mode 100644 system/src/Grav/Framework/Pagination/AbstractPagination.php create mode 100644 system/src/Grav/Framework/Pagination/AbstractPaginationPage.php create mode 100644 system/src/Grav/Framework/Pagination/Interfaces/PaginationInterface.php create mode 100644 system/src/Grav/Framework/Pagination/Interfaces/PaginationPageInterface.php create mode 100644 system/src/Grav/Framework/Pagination/Pagination.php create mode 100644 system/src/Grav/Framework/Pagination/PaginationPage.php create mode 100644 system/src/Grav/Framework/Psr7/AbstractUri.php create mode 100644 system/src/Grav/Framework/Psr7/Request.php create mode 100644 system/src/Grav/Framework/Psr7/Response.php create mode 100644 system/src/Grav/Framework/Psr7/ServerRequest.php create mode 100644 system/src/Grav/Framework/Psr7/Stream.php create mode 100644 system/src/Grav/Framework/Psr7/Traits/MessageDecoratorTrait.php create mode 100644 system/src/Grav/Framework/Psr7/Traits/RequestDecoratorTrait.php create mode 100644 system/src/Grav/Framework/Psr7/Traits/ResponseDecoratorTrait.php create mode 100644 system/src/Grav/Framework/Psr7/Traits/ServerRequestDecoratorTrait.php create mode 100644 system/src/Grav/Framework/Psr7/Traits/StreamDecoratorTrait.php create mode 100644 system/src/Grav/Framework/Psr7/Traits/UploadedFileDecoratorTrait.php create mode 100644 system/src/Grav/Framework/Psr7/Traits/UriDecorationTrait.php create mode 100644 system/src/Grav/Framework/Psr7/UploadedFile.php create mode 100644 system/src/Grav/Framework/Psr7/Uri.php create mode 100644 system/src/Grav/Framework/RequestHandler/Exception/InvalidArgumentException.php create mode 100644 system/src/Grav/Framework/RequestHandler/Exception/NotFoundException.php create mode 100644 system/src/Grav/Framework/RequestHandler/Exception/NotHandledException.php create mode 100644 system/src/Grav/Framework/RequestHandler/Exception/PageExpiredException.php create mode 100644 system/src/Grav/Framework/RequestHandler/Exception/RequestException.php create mode 100644 system/src/Grav/Framework/RequestHandler/Middlewares/Exceptions.php create mode 100644 system/src/Grav/Framework/RequestHandler/RequestHandler.php create mode 100644 system/src/Grav/Framework/RequestHandler/Traits/RequestHandlerTrait.php create mode 100644 system/src/Grav/Framework/Route/Route.php create mode 100644 system/src/Grav/Framework/Route/RouteFactory.php create mode 100644 system/src/Grav/Framework/Session/Exceptions/SessionException.php create mode 100644 system/src/Grav/Framework/Session/Messages.php create mode 100644 system/src/Grav/Framework/Session/Session.php create mode 100644 system/src/Grav/Framework/Session/SessionInterface.php create mode 100644 system/src/Grav/Framework/Uri/Uri.php create mode 100644 system/src/Grav/Framework/Uri/UriFactory.php create mode 100644 system/src/Grav/Framework/Uri/UriPartsFilter.php create mode 100644 system/src/Grav/Installer/Install.php create mode 100644 system/src/Grav/Installer/InstallException.php create mode 100644 system/src/Grav/Installer/VersionUpdate.php create mode 100644 system/src/Grav/Installer/VersionUpdater.php create mode 100644 system/src/Grav/Installer/Versions.php create mode 100644 system/src/Grav/Installer/YamlUpdater.php create mode 100644 system/src/Grav/Installer/updates/1.7.0_2020-11-20_1.php create mode 100644 system/src/Twig/DeferredExtension/DeferredBlockNode.php create mode 100644 system/src/Twig/DeferredExtension/DeferredExtension.php create mode 100644 system/src/Twig/DeferredExtension/DeferredExtensionNode.php create mode 100644 system/src/Twig/DeferredExtension/DeferredNode.php create mode 100644 system/src/Twig/DeferredExtension/DeferredNodeVisitor.php create mode 100644 system/src/Twig/DeferredExtension/DeferredNodeVisitorCompat.php create mode 100644 system/src/Twig/DeferredExtension/DeferredTokenParser.php create mode 100644 system/templates/default.html.twig create mode 100644 system/templates/external.html.twig create mode 100644 system/templates/flex/404.html.twig create mode 100644 system/templates/flex/_default/collection/debug.html.twig create mode 100644 system/templates/flex/_default/object/debug.html.twig create mode 100644 system/templates/modular/default.html.twig create mode 100644 system/templates/partials/messages.html.twig create mode 100644 system/templates/partials/metadata.html.twig create mode 100644 tests/_bootstrap.php create mode 100644 tests/_support/AcceptanceTester.php create mode 100644 tests/_support/FunctionalTester.php create mode 100644 tests/_support/Helper/Acceptance.php create mode 100644 tests/_support/Helper/Functional.php create mode 100644 tests/_support/Helper/Unit.php create mode 100644 tests/_support/UnitTester.php create mode 100644 tests/acceptance.suite.yml create mode 100644 tests/acceptance/_bootstrap.php create mode 100644 tests/fake/nested-site/user/pages/01.item1/01.item1-1/01.item1-1-1/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/01.item1-1/02.item1-1-2/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/01.item1-1/03.item1-1-3/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/01.item1-1/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/02.item1-2/01.item1-2-1/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/02.item1-2/02.item1-2-2/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/02.item1-2/03.item1-2-3/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/02.item1-2/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/03.item1-3/01.item1-3-1/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/03.item1-3/02.item1-3-2/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/03.item1-3/03.item1-3-3/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/03.item1-3/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/default.md create mode 100644 tests/fake/nested-site/user/pages/01.item1/existing-file.zip create mode 100644 tests/fake/nested-site/user/pages/01.item1/home-cache-image.jpg create mode 100644 tests/fake/nested-site/user/pages/01.item1/home-sample-image.jpg create mode 100644 tests/fake/nested-site/user/pages/02.item2/01.item2-1/01.item2-1-1/default.md create mode 100644 tests/fake/nested-site/user/pages/02.item2/01.item2-1/02.item2-1-2/default.md create mode 100644 tests/fake/nested-site/user/pages/02.item2/01.item2-1/03.item2-1-3/default.md create mode 100644 tests/fake/nested-site/user/pages/02.item2/01.item2-1/default.md create mode 100644 tests/fake/nested-site/user/pages/02.item2/02.item2-2/01.item2-2-1/default.md create mode 100644 tests/fake/nested-site/user/pages/02.item2/02.item2-2/02.item2-2-2/default.md create mode 100644 tests/fake/nested-site/user/pages/02.item2/02.item2-2/03.item2-2-3/default.md create mode 100644 tests/fake/nested-site/user/pages/02.item2/02.item2-2/cache-image.jpg create mode 100644 tests/fake/nested-site/user/pages/02.item2/02.item2-2/default.md create mode 100644 tests/fake/nested-site/user/pages/02.item2/02.item2-2/existing-file.zip create mode 100644 tests/fake/nested-site/user/pages/02.item2/02.item2-2/sample-image.jpg create mode 100644 tests/fake/nested-site/user/pages/02.item2/03.item2-3/01.item2-3-1/default.md create mode 100644 tests/fake/nested-site/user/pages/02.item2/03.item2-3/02.item2-3-2/default.md create mode 100644 tests/fake/nested-site/user/pages/02.item2/03.item2-3/03.item2-3-3/default.md create mode 100644 tests/fake/nested-site/user/pages/02.item2/03.item2-3/default.md create mode 100644 tests/fake/nested-site/user/pages/02.item2/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/01.item3-1/01.item3-1-1/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/01.item3-1/02.item3-1-2/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/01.item3-1/03.item3-1-3/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/01.item3-1/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/02.item3-2/01.item3-2-1/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/02.item3-2/02.item3-2-2/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/02.item3-2/03.item3-2-3/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/02.item3-2/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/03.item3-3/01.item3-3-1/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/03.item3-3/02.item3-3-2/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/03.item3-3/03.item3-3-3/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/03.item3-3/default.md create mode 100644 tests/fake/nested-site/user/pages/03.item3/default.md create mode 100644 tests/fake/simple-site/user/pages/01.home/default.md create mode 100644 tests/fake/simple-site/user/pages/02.blog/blog.md create mode 100644 tests/fake/simple-site/user/pages/02.blog/post-one/item.md create mode 100644 tests/fake/simple-site/user/pages/02.blog/post-two/item.md create mode 100644 tests/fake/simple-site/user/pages/03.about/default.md create mode 100644 tests/fake/single-pages/01.simple-page/default.md create mode 100644 tests/functional.suite.yml create mode 100644 tests/functional/Grav/Console/DirectInstallCommandTest.php create mode 100644 tests/functional/_bootstrap.php create mode 100644 tests/phpstan/phpstan-bootstrap.php create mode 100644 tests/phpstan/phpstan.neon create mode 100644 tests/phpstan/plugins-bootstrap.php create mode 100644 tests/phpstan/plugins.neon create mode 100644 tests/unit.suite.yml create mode 100644 tests/unit/Grav/Common/AssetsTest.php create mode 100644 tests/unit/Grav/Common/BrowserTest.php create mode 100644 tests/unit/Grav/Common/ComposerTest.php create mode 100644 tests/unit/Grav/Common/GPM/GPMTest.php create mode 100644 tests/unit/Grav/Common/Helpers/ExcerptsTest.php create mode 100644 tests/unit/Grav/Common/InflectorTest.php create mode 100644 tests/unit/Grav/Common/Language/LanguageCodesTest.php create mode 100644 tests/unit/Grav/Common/Markdown/ParsedownTest.php create mode 100644 tests/unit/Grav/Common/Page/PagesTest.php create mode 100644 tests/unit/Grav/Common/Twig/TwigExtensionTest.php create mode 100644 tests/unit/Grav/Common/UriTest.php create mode 100644 tests/unit/Grav/Common/UtilsTest.php create mode 100644 tests/unit/Grav/Console/Gpm/InstallCommandTest.php create mode 100644 tests/unit/Grav/Framework/File/Formatter/CsvFormatterTest.php create mode 100644 tests/unit/Grav/Framework/Filesystem/FilesystemTest.php create mode 100644 tests/unit/_bootstrap.php create mode 100644 tmp/.gitkeep create mode 100644 user/config/backups.yaml create mode 100644 user/config/groups.yaml create mode 100644 user/config/media.yaml create mode 100644 user/config/plugins/imgsrcset.yaml create mode 100644 user/config/plugins/lazy-image.yaml create mode 100644 user/config/plugins/sitemap.yaml create mode 100644 user/config/scheduler.yaml create mode 100644 user/config/site.yaml create mode 100644 user/config/streams.yaml create mode 100644 user/config/system.yaml create mode 100644 user/config/versions.yaml create mode 100644 user/data/.gitkeep create mode 100644 user/localhost/config/plugins/aura.yaml create mode 100644 user/localhost/config/plugins/automagic-images.yaml create mode 100644 user/localhost/config/plugins/lazy-image.yaml create mode 100644 user/localhost/config/site.yaml create mode 100644 user/localhost/config/system.yaml create mode 100644 user/plugins/.gitkeep create mode 100644 user/plugins/admin/.editorconfig create mode 100644 user/plugins/admin/.gitattributes create mode 100644 user/plugins/admin/.github/FUNDING.yml create mode 100644 user/plugins/admin/.gitignore create mode 100644 user/plugins/admin/CHANGELOG.md create mode 100644 user/plugins/admin/CONTRIBUTING.md create mode 100644 user/plugins/admin/LICENSE create mode 100644 user/plugins/admin/README.md create mode 100644 user/plugins/admin/UPGRADE.md create mode 100644 user/plugins/admin/admin.php create mode 100644 user/plugins/admin/admin.yaml create mode 100644 user/plugins/admin/assets/admin-dashboard.png create mode 100644 user/plugins/admin/blueprints.yaml create mode 100644 user/plugins/admin/blueprints/admin/pages/copy.yaml create mode 100644 user/plugins/admin/blueprints/admin/pages/modular_new.yaml create mode 100644 user/plugins/admin/blueprints/admin/pages/modular_raw.yaml create mode 100644 user/plugins/admin/blueprints/admin/pages/move.yaml create mode 100644 user/plugins/admin/blueprints/admin/pages/new.yaml create mode 100644 user/plugins/admin/blueprints/admin/pages/new_folder.yaml create mode 100644 user/plugins/admin/blueprints/admin/pages/raw.yaml create mode 100644 user/plugins/admin/blueprints/admin/pages/root_raw.yaml create mode 100644 user/plugins/admin/blueprints/config/media.yaml create mode 100644 user/plugins/admin/classes/plugin/Admin.php create mode 100644 user/plugins/admin/classes/plugin/AdminBaseController.php create mode 100644 user/plugins/admin/classes/plugin/AdminController.php create mode 100644 user/plugins/admin/classes/plugin/AdminForm.php create mode 100644 user/plugins/admin/classes/plugin/AdminFormFactory.php create mode 100644 user/plugins/admin/classes/plugin/Controllers/AbstractController.php create mode 100644 user/plugins/admin/classes/plugin/Controllers/AdminController.php create mode 100644 user/plugins/admin/classes/plugin/Controllers/Login/LoginController.php create mode 100644 user/plugins/admin/classes/plugin/Gpm.php create mode 100644 user/plugins/admin/classes/plugin/Popularity.php create mode 100644 user/plugins/admin/classes/plugin/Router.php create mode 100644 user/plugins/admin/classes/plugin/Routers/LoginRouter.php create mode 100644 user/plugins/admin/classes/plugin/ScssCompiler.php create mode 100644 user/plugins/admin/classes/plugin/ScssList.php create mode 100644 user/plugins/admin/classes/plugin/Themes.php create mode 100644 user/plugins/admin/classes/plugin/Twig/AdminTwigExtension.php create mode 100644 user/plugins/admin/classes/plugin/Utils.php create mode 100644 user/plugins/admin/classes/plugin/WhiteLabel.php create mode 100644 user/plugins/admin/codeception.yml create mode 100644 user/plugins/admin/composer.json create mode 100644 user/plugins/admin/composer.lock create mode 100644 user/plugins/admin/hebe.json create mode 100644 user/plugins/admin/languages/ar.yaml create mode 100644 user/plugins/admin/languages/bg.yaml create mode 100644 user/plugins/admin/languages/bn.yaml create mode 100644 user/plugins/admin/languages/br.yaml create mode 100644 user/plugins/admin/languages/ca.yaml create mode 100644 user/plugins/admin/languages/cs.yaml create mode 100644 user/plugins/admin/languages/cy.yaml create mode 100644 user/plugins/admin/languages/da.yaml create mode 100644 user/plugins/admin/languages/de.yaml create mode 100644 user/plugins/admin/languages/el.yaml create mode 100644 user/plugins/admin/languages/en.yaml create mode 100644 user/plugins/admin/languages/eo.yaml create mode 100644 user/plugins/admin/languages/es.yaml create mode 100644 user/plugins/admin/languages/et.yaml create mode 100644 user/plugins/admin/languages/eu.yaml create mode 100644 user/plugins/admin/languages/fa.yaml create mode 100644 user/plugins/admin/languages/fi.yaml create mode 100644 user/plugins/admin/languages/fr.yaml create mode 100644 user/plugins/admin/languages/gl.yaml create mode 100644 user/plugins/admin/languages/he.yaml create mode 100644 user/plugins/admin/languages/hr.yaml create mode 100644 user/plugins/admin/languages/hu.yaml create mode 100644 user/plugins/admin/languages/id.yaml create mode 100644 user/plugins/admin/languages/it.yaml create mode 100644 user/plugins/admin/languages/ja.yaml create mode 100644 user/plugins/admin/languages/ko.yaml create mode 100644 user/plugins/admin/languages/ku.yaml create mode 100644 user/plugins/admin/languages/lt.yaml create mode 100644 user/plugins/admin/languages/mn.yaml create mode 100644 user/plugins/admin/languages/nl.yaml create mode 100644 user/plugins/admin/languages/no.yaml create mode 100644 user/plugins/admin/languages/pl.yaml create mode 100644 user/plugins/admin/languages/pt.yaml create mode 100644 user/plugins/admin/languages/ro.yaml create mode 100644 user/plugins/admin/languages/ru.yaml create mode 100644 user/plugins/admin/languages/si.yaml create mode 100644 user/plugins/admin/languages/sk.yaml create mode 100644 user/plugins/admin/languages/sl.yaml create mode 100644 user/plugins/admin/languages/sr.yaml create mode 100644 user/plugins/admin/languages/sv.yaml create mode 100644 user/plugins/admin/languages/th.yaml create mode 100644 user/plugins/admin/languages/tlh.yaml create mode 100644 user/plugins/admin/languages/tr.yaml create mode 100644 user/plugins/admin/languages/uk.yaml create mode 100644 user/plugins/admin/languages/vi.yaml create mode 100644 user/plugins/admin/languages/zh-cn.yaml create mode 100644 user/plugins/admin/languages/zh-tw.yaml create mode 100644 user/plugins/admin/languages/zh.yaml create mode 100644 user/plugins/admin/pages/admin/ajax.md create mode 100644 user/plugins/admin/pages/admin/backup.md create mode 100644 user/plugins/admin/pages/admin/cache.md create mode 100644 user/plugins/admin/pages/admin/changelog.md create mode 100644 user/plugins/admin/pages/admin/config.md create mode 100644 user/plugins/admin/pages/admin/dashboard.md create mode 100644 user/plugins/admin/pages/admin/denied.md create mode 100644 user/plugins/admin/pages/admin/error.md create mode 100644 user/plugins/admin/pages/admin/feed.md create mode 100644 user/plugins/admin/pages/admin/forgot.md create mode 100644 user/plugins/admin/pages/admin/info.md create mode 100644 user/plugins/admin/pages/admin/installer.md create mode 100644 user/plugins/admin/pages/admin/login.md create mode 100644 user/plugins/admin/pages/admin/media.md create mode 100644 user/plugins/admin/pages/admin/notifications.md create mode 100644 user/plugins/admin/pages/admin/pages-filter.md create mode 100644 user/plugins/admin/pages/admin/pages.md create mode 100644 user/plugins/admin/pages/admin/plugins.md create mode 100644 user/plugins/admin/pages/admin/preview.md create mode 100644 user/plugins/admin/pages/admin/register.md create mode 100644 user/plugins/admin/pages/admin/reset.md create mode 100644 user/plugins/admin/pages/admin/site.md create mode 100644 user/plugins/admin/pages/admin/statistics.md create mode 100644 user/plugins/admin/pages/admin/system.md create mode 100644 user/plugins/admin/pages/admin/themes.md create mode 100644 user/plugins/admin/pages/admin/tools.md create mode 100644 user/plugins/admin/pages/admin/update.md create mode 100644 user/plugins/admin/pages/admin/user.md create mode 100644 user/plugins/admin/permissions.yaml create mode 100644 user/plugins/admin/presets.yaml create mode 100644 user/plugins/admin/tests/_bootstrap.php create mode 100644 user/plugins/admin/tests/_support/Helper/Unit.php create mode 100644 user/plugins/admin/tests/_support/UnitTester.php create mode 100644 user/plugins/admin/tests/unit.suite.yml create mode 100644 user/plugins/admin/tests/unit/_bootstrap.php create mode 100644 user/plugins/admin/tests/unit/classes/controllerTest.php create mode 100644 user/plugins/admin/themes/grav/.babelrc create mode 100644 user/plugins/admin/themes/grav/.eslintrc create mode 100644 user/plugins/admin/themes/grav/app/dashboard/backup.js create mode 100644 user/plugins/admin/themes/grav/app/dashboard/cache.js create mode 100644 user/plugins/admin/themes/grav/app/dashboard/chart.js create mode 100644 user/plugins/admin/themes/grav/app/dashboard/index.js create mode 100644 user/plugins/admin/themes/grav/app/dashboard/update.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/acl-picker.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/array.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/collections.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/colorpicker.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/cron.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/datetime.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/editor.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/editor/buttons.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/filepicker.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/files.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/folder.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/iconpicker.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/indeterminate.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/index.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/mediapicker.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/multilevel.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/parents.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/permissions.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/range.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/selectize.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/selectunique.js create mode 100644 user/plugins/admin/themes/grav/app/forms/fields/text.js create mode 100644 user/plugins/admin/themes/grav/app/forms/form.js create mode 100644 user/plugins/admin/themes/grav/app/forms/index.js create mode 100644 user/plugins/admin/themes/grav/app/forms/state.js create mode 100644 user/plugins/admin/themes/grav/app/main.js create mode 100644 user/plugins/admin/themes/grav/app/media/index.js create mode 100644 user/plugins/admin/themes/grav/app/pages/filter.js create mode 100644 user/plugins/admin/themes/grav/app/pages/index.js create mode 100644 user/plugins/admin/themes/grav/app/pages/page/add.js create mode 100644 user/plugins/admin/themes/grav/app/pages/page/delete.js create mode 100644 user/plugins/admin/themes/grav/app/pages/page/disable-buttons.js create mode 100644 user/plugins/admin/themes/grav/app/pages/page/index.js create mode 100644 user/plugins/admin/themes/grav/app/pages/page/media.js create mode 100644 user/plugins/admin/themes/grav/app/pages/page/move.js create mode 100644 user/plugins/admin/themes/grav/app/pages/page/multilang.js create mode 100644 user/plugins/admin/themes/grav/app/pages/page/unset.js create mode 100644 user/plugins/admin/themes/grav/app/pages/tree.js create mode 100644 user/plugins/admin/themes/grav/app/plugins/index.js create mode 100644 user/plugins/admin/themes/grav/app/themes/index.js create mode 100644 user/plugins/admin/themes/grav/app/tools/index.js create mode 100644 user/plugins/admin/themes/grav/app/tools/logs.js create mode 100644 user/plugins/admin/themes/grav/app/updates/channel-switcher.js create mode 100644 user/plugins/admin/themes/grav/app/updates/check.js create mode 100644 user/plugins/admin/themes/grav/app/updates/feed.js create mode 100644 user/plugins/admin/themes/grav/app/updates/index.js create mode 100644 user/plugins/admin/themes/grav/app/updates/notifications.js create mode 100644 user/plugins/admin/themes/grav/app/updates/update.js create mode 100644 user/plugins/admin/themes/grav/app/utils/2fa.js create mode 100644 user/plugins/admin/themes/grav/app/utils/bootstrap-collapse.js create mode 100644 user/plugins/admin/themes/grav/app/utils/bootstrap-datetimepicker.js create mode 100644 user/plugins/admin/themes/grav/app/utils/bootstrap-dropdown.js create mode 100644 user/plugins/admin/themes/grav/app/utils/bootstrap-transition.js create mode 100644 user/plugins/admin/themes/grav/app/utils/changelog.js create mode 100644 user/plugins/admin/themes/grav/app/utils/colors.js create mode 100644 user/plugins/admin/themes/grav/app/utils/cookies.js create mode 100644 user/plugins/admin/themes/grav/app/utils/cron-ui.js create mode 100644 user/plugins/admin/themes/grav/app/utils/finderjs.js create mode 100644 user/plugins/admin/themes/grav/app/utils/formatbytes.js create mode 100644 user/plugins/admin/themes/grav/app/utils/gpm.js create mode 100644 user/plugins/admin/themes/grav/app/utils/jquery-utils.js create mode 100644 user/plugins/admin/themes/grav/app/utils/keepalive.js create mode 100644 user/plugins/admin/themes/grav/app/utils/offline.js create mode 100644 user/plugins/admin/themes/grav/app/utils/packages.js create mode 100644 user/plugins/admin/themes/grav/app/utils/remodal.js create mode 100644 user/plugins/admin/themes/grav/app/utils/request.js create mode 100644 user/plugins/admin/themes/grav/app/utils/response.js create mode 100644 user/plugins/admin/themes/grav/app/utils/scrollbar.js create mode 100644 user/plugins/admin/themes/grav/app/utils/selectize-option-click.js create mode 100644 user/plugins/admin/themes/grav/app/utils/selectize-required-fix.js create mode 100644 user/plugins/admin/themes/grav/app/utils/sidebar.js create mode 100644 user/plugins/admin/themes/grav/app/utils/storage.js create mode 100644 user/plugins/admin/themes/grav/app/utils/tabs-memory.js create mode 100644 user/plugins/admin/themes/grav/app/utils/toastr.js create mode 100644 user/plugins/admin/themes/grav/app/whitelabel/compile.js create mode 100644 user/plugins/admin/themes/grav/app/whitelabel/index.js create mode 100644 user/plugins/admin/themes/grav/app/whitelabel/presets.js create mode 100644 user/plugins/admin/themes/grav/css-compiled/fonts.css create mode 100644 user/plugins/admin/themes/grav/css-compiled/fonts.css.map create mode 100644 user/plugins/admin/themes/grav/css-compiled/nucleus.css create mode 100644 user/plugins/admin/themes/grav/css-compiled/nucleus.css.map create mode 100644 user/plugins/admin/themes/grav/css-compiled/paper.css.map create mode 100644 user/plugins/admin/themes/grav/css-compiled/preset.css create mode 100644 user/plugins/admin/themes/grav/css-compiled/preset.css.map create mode 100644 user/plugins/admin/themes/grav/css-compiled/simple-fonts.css create mode 100644 user/plugins/admin/themes/grav/css-compiled/simple-fonts.css.map create mode 100644 user/plugins/admin/themes/grav/css-compiled/template.css create mode 100644 user/plugins/admin/themes/grav/css-compiled/template.css.map create mode 100644 user/plugins/admin/themes/grav/css/chartist.min.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/codemirror.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/3024-day.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/3024-night.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/abcdef.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/ambiance-mobile.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/ambiance.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/ayu-dark.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/ayu-mirage.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/base16-dark.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/base16-light.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/bespin.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/blackboard.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/cobalt.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/colorforth.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/darcula.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/dracula.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/duotone-dark.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/duotone-light.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/eclipse.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/elegant.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/erlang-dark.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/gruvbox-dark.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/hopscotch.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/icecoder.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/idea.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/isotope.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/lesser-dark.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/liquibyte.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/lucario.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/material-darker.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/material-ocean.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/material-palenight.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/material.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/mbo.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/mdn-like.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/midnight.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/monokai.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/moxer.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/neat.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/neo.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/night.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/nord.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/oceanic-next.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/panda-syntax.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/paper.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/paraiso-dark.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/paraiso-light.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/pastel-on-dark.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/railscasts.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/rubyblue.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/seti.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/shadowfox.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/solarized.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/ssms.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/the-matrix.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/tomorrow-night-bright.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/tomorrow-night-eighties.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/ttcn.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/twilight.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/vibrant-ink.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/xq-dark.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/xq-light.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/yeti.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/yonce.css create mode 100644 user/plugins/admin/themes/grav/css/codemirror/themes/zenburn.css create mode 100644 user/plugins/admin/themes/grav/css/featherlight.min.css create mode 100644 user/plugins/admin/themes/grav/css/fork-awesome.min.css create mode 100644 user/plugins/admin/themes/grav/css/hint.base.min.css create mode 100644 user/plugins/admin/themes/grav/css/iconpicker.css create mode 100644 user/plugins/admin/themes/grav/css/nucleus-ie10.css create mode 100644 user/plugins/admin/themes/grav/css/nucleus-ie9.css create mode 100644 user/plugins/admin/themes/grav/css/prism.css create mode 100644 user/plugins/admin/themes/grav/css/pure-0.5.0/grids-min.css create mode 100644 user/plugins/admin/themes/grav/css/rtl.css create mode 100644 user/plugins/admin/themes/grav/css/selectize.min.css create mode 100644 user/plugins/admin/themes/grav/fonts/forkawesome-webfont.eot create mode 100644 user/plugins/admin/themes/grav/fonts/forkawesome-webfont.svg create mode 100644 user/plugins/admin/themes/grav/fonts/forkawesome-webfont.ttf create mode 100644 user/plugins/admin/themes/grav/fonts/forkawesome-webfont.woff create mode 100644 user/plugins/admin/themes/grav/fonts/forkawesome-webfont.woff2 create mode 100644 user/plugins/admin/themes/grav/fonts/rockettheme-apps/rockettheme-apps.eot create mode 100644 user/plugins/admin/themes/grav/fonts/rockettheme-apps/rockettheme-apps.svg create mode 100644 user/plugins/admin/themes/grav/fonts/rockettheme-apps/rockettheme-apps.ttf create mode 100644 user/plugins/admin/themes/grav/fonts/rockettheme-apps/rockettheme-apps.woff create mode 100644 user/plugins/admin/themes/grav/images/favicon.png create mode 100644 user/plugins/admin/themes/grav/images/grav-regular.svg create mode 100644 user/plugins/admin/themes/grav/images/grav-small.svg create mode 100644 user/plugins/admin/themes/grav/images/logo.svg create mode 100644 user/plugins/admin/themes/grav/images/transparent.png create mode 100644 user/plugins/admin/themes/grav/js/admin.min.js create mode 100644 user/plugins/admin/themes/grav/js/form-attr.polyfill.js create mode 100644 user/plugins/admin/themes/grav/js/vendor.min.js create mode 100644 user/plugins/admin/themes/grav/package-lock.json create mode 100644 user/plugins/admin/themes/grav/package.json create mode 100644 user/plugins/admin/themes/grav/scss/configuration/nucleus/_base.scss create mode 100644 user/plugins/admin/themes/grav/scss/configuration/nucleus/_breakpoints.scss create mode 100644 user/plugins/admin/themes/grav/scss/configuration/nucleus/_core.scss create mode 100644 user/plugins/admin/themes/grav/scss/configuration/nucleus/_layout.scss create mode 100644 user/plugins/admin/themes/grav/scss/configuration/nucleus/_nav.scss create mode 100644 user/plugins/admin/themes/grav/scss/configuration/nucleus/_typography.scss create mode 100644 user/plugins/admin/themes/grav/scss/configuration/template/_base.scss create mode 100644 user/plugins/admin/themes/grav/scss/configuration/template/_colors.scss create mode 100644 user/plugins/admin/themes/grav/scss/configuration/template/_variables.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus/_core.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus/_flex.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus/_forms.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus/_typography.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus/functions/_base.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus/functions/_direction.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus/functions/_range.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus/mixins/_base.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus/mixins/_breakpoints.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus/mixins/_utilities.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus/particles/_align-text.scss create mode 100644 user/plugins/admin/themes/grav/scss/nucleus/particles/_visibility.scss create mode 100644 user/plugins/admin/themes/grav/scss/preset.scss create mode 100644 user/plugins/admin/themes/grav/scss/simple-fonts.scss create mode 100644 user/plugins/admin/themes/grav/scss/template.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_admin.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_buttons.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_changelog.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_colors.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_core.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_custom.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_dropzone.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_editor.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_errors.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_extensions.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_fonts.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_forms.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_gpm.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_header.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_horizontal-scroll.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_jqcron.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_login.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_media.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_nav-toggle.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_notifications.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_pages.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_phpinfo.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_remodal.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_scrollbars.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_tables.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_tabs.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_toastr.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_tools.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_typography.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_utilities.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/_whitelabel.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/modules/_base.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/modules/_bourbon_essentials.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/modules/_buttons.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/modules/_colorpicker.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/modules/_datetimepicker.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/modules/_finderjs.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/modules/_input-range.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/modules/_scrollbars.scss create mode 100644 user/plugins/admin/themes/grav/scss/template/modules/_toggle-switch.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/_bourbon-deprecated-upcoming.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/_bourbon.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_button.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_clearfix.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_directional-values.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_ellipsis.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_font-family.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_hide-text.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_html5-input-types.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_position.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_prefixer.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_rem.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_retina-image.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_size.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_timing-functions.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_triangle.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/addons/_word-wrap.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_animation.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_appearance.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_backface-visibility.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_background-image.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_background.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_border-image.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_border-radius.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_box-sizing.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_calc.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_columns.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_filter.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_flex-box.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_font-face.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_font-feature-settings.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_hidpi-media-query.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_hyphens.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_image-rendering.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_keyframes.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_linear-gradient.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_perspective.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_placeholder.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_radial-gradient.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_transform.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_transition.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/css3/_user-select.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/functions/_assign.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/functions/_color-lightness.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/functions/_flex-grid.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/functions/_golden-ratio.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/functions/_grid-width.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/functions/_modular-scale.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/functions/_px-to-em.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/functions/_px-to-rem.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/functions/_strip-units.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/functions/_tint-shade.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/functions/_transition-property-name.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/functions/_unpack.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_convert-units.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_gradient-positions-parser.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_is-num.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_linear-angle-parser.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_linear-gradient-parser.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_linear-positions-parser.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_linear-side-corner-parser.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_radial-arg-parser.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_radial-gradient-parser.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_radial-positions-parser.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_render-gradients.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_shape-size-stripper.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/helpers/_str-to-num.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/settings/_prefixer.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/bourbon/settings/_px-to-em.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/_color-schemer.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/color-schemer/_cmyk.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/color-schemer/_color-adjustments.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/color-schemer/_color-schemer.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/color-schemer/_colorblind.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/color-schemer/_comparison.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/color-schemer/_equalize.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/color-schemer/_harmonize.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/color-schemer/_interpolation.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/color-schemer/_mix.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/color-schemer/_mixins.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/color-schemer/_ryb.scss create mode 100644 user/plugins/admin/themes/grav/scss/vendor/color-schemer/color-schemer/_tint-shade.scss create mode 100644 user/plugins/admin/themes/grav/templates/changelog.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/config.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/dashboard.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/default.json.twig create mode 100644 user/plugins/admin/themes/grav/templates/denied.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/email/base.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/empty.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/error.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/error.json.twig create mode 100644 user/plugins/admin/themes/grav/templates/forgot.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/field.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/acl_picker/acl_picker.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/array/array.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/backupshistory/backupshistory.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/blueprint/blueprint.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/codemirror/codemirror.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/colorpicker/colorpicker.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/colorscheme.color/colorscheme.color.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/colorscheme/colorscheme.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/column/column.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/columns/columns.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/cron/cron.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/cronstatus/cronstatus.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/dateformat/dateformat.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/datetime/datetime.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/editor/editor.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/fieldset/fieldset.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/file/file.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/filepicker/filepicker.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/folder-slug/folder-slug.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/frontmatter/frontmatter.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/iconpicker/iconpicker.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/iconpicker/icons.yaml create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/list/list.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/markdown/markdown.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/mediapicker/mediapicker.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/multilevel/multilevel.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/order/order.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/pagemedia/pagemedia.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/pagemediaselect/pagemediaselect.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/pages/pages.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/parents/parents.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/permissions/permissions.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/range/range.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/section/section.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/selectize/selectize.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/selectunique/selectunique.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/taxonomy/taxonomy.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/themepreview/themepreview.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/themeselect/themeselect.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/userinfo/userinfo.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/widgets/widgets.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/forms/fields/xss/xss.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/installer.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/login.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/media-list-content.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/pages.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/backups-button.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/base-root.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/base.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/blueprints-copy.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/blueprints-new-folder.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/blueprints-new.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/blueprints-raw.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/blueprints.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/dashboard-feed.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/dashboard-maintenance.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/dashboard-notifications.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/dashboard-pages.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/dashboard-problems.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/dashboard-statistics.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/feed-block.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/footer.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/header.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/javascript-config.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/javascripts.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/list-sort.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/login-form.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/login-logo.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/login-logout.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/login-twofa.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/login.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/logo.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/media-list-wrapper.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/media-list-wrapper__list.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/media-list-wrapper__list__filters.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/media-list-wrapper__sidebar.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/messages.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/modal-add-package.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/modal-changelog.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/modal-changes-detected.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/modal-reinstall-package.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/modal-remove-package.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/modal-switch-theme.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/modal-update-packages.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/nav-quick-tray.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/nav-toggle.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/nav-user-avatar.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/nav-user-details.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/nav.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/notification-dashboard-block.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/notification-feed-block.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/notification-plugins-block.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/notification-themes-block.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/notification-top-block.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/page-children.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/page-legend.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/page-move.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/plugin-data.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/plugins-details.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/plugins-list.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/register.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/release-toggle.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/spinning-wheel.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/stylesheets.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/theme.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/themes-details.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/themes-list.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/tools-backups-titlebar.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/tools-backups.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/tools-direct-install.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/tools-logs.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/tools-reports.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/tools-scheduler-titlebar.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/tools-scheduler.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/userinfo-avatar-credit.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/partials/userinfo-avatar.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/plugins.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/plugins/admin-buttons.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/preview.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/register.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/reports/security.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/reports/yamllinter.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/reset.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/statistics.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/themes.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/tools.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/unauthorized.html.twig create mode 100644 user/plugins/admin/themes/grav/templates/user.html.twig create mode 100644 user/plugins/admin/themes/grav/theme.yaml create mode 100644 user/plugins/admin/themes/grav/watch.sh create mode 100644 user/plugins/admin/themes/grav/webpack.conf.js create mode 100644 user/plugins/admin/themes/grav/yarn.lock create mode 100644 user/plugins/admin/twig/AdminTwigExtension.php create mode 100644 user/plugins/admin/vendor/autoload.php create mode 100644 user/plugins/admin/vendor/bin/picofeed create mode 100644 user/plugins/admin/vendor/bin/pscss create mode 100644 user/plugins/admin/vendor/composer/ClassLoader.php create mode 100644 user/plugins/admin/vendor/composer/InstalledVersions.php create mode 100644 user/plugins/admin/vendor/composer/LICENSE create mode 100644 user/plugins/admin/vendor/composer/autoload_classmap.php create mode 100644 user/plugins/admin/vendor/composer/autoload_files.php create mode 100644 user/plugins/admin/vendor/composer/autoload_namespaces.php create mode 100644 user/plugins/admin/vendor/composer/autoload_psr4.php create mode 100644 user/plugins/admin/vendor/composer/autoload_real.php create mode 100644 user/plugins/admin/vendor/composer/autoload_static.php create mode 100644 user/plugins/admin/vendor/composer/installed.json create mode 100644 user/plugins/admin/vendor/composer/installed.php create mode 100644 user/plugins/admin/vendor/composer/platform_check.php create mode 100644 user/plugins/admin/vendor/laminas/laminas-xml/.github/workflows/continuous-integration.yml create mode 100644 user/plugins/admin/vendor/laminas/laminas-xml/.github/workflows/release-on-milestone-closed.yml create mode 100644 user/plugins/admin/vendor/laminas/laminas-xml/COPYRIGHT.md create mode 100644 user/plugins/admin/vendor/laminas/laminas-xml/LICENSE.md create mode 100644 user/plugins/admin/vendor/laminas/laminas-xml/README.md create mode 100644 user/plugins/admin/vendor/laminas/laminas-xml/composer.json create mode 100644 user/plugins/admin/vendor/laminas/laminas-xml/composer.lock create mode 100644 user/plugins/admin/vendor/laminas/laminas-xml/src/Exception/ExceptionInterface.php create mode 100644 user/plugins/admin/vendor/laminas/laminas-xml/src/Exception/InvalidArgumentException.php create mode 100644 user/plugins/admin/vendor/laminas/laminas-xml/src/Exception/RuntimeException.php create mode 100644 user/plugins/admin/vendor/laminas/laminas-xml/src/Security.php create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/.github/FUNDING.yml create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/.github/workflows/continuous-integration.yml create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/.github/workflows/release-on-milestone-closed.yml create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/.laminas-ci.json create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/COPYRIGHT.md create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/LICENSE.md create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/README.md create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/composer.json create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/composer.lock create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/config/replacements.php create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/psalm-baseline.xml create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/psalm.xml.dist create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/src/Autoloader.php create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/src/ConfigPostProcessor.php create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/src/Module.php create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/src/Replacements.php create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/src/RewriteRules.php create mode 100644 user/plugins/admin/vendor/laminas/laminas-zendframework-bridge/src/autoload.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/LICENSE create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Base.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/Client.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/ClientException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/Curl.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/ForbiddenException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/HttpHeaders.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/InvalidCertificateException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/InvalidUrlException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/MaxRedirectException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/MaxSizeException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/Stream.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/TimeoutException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/UnauthorizedException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Client/Url.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Config/Config.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Encoding/Encoding.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Filter/Attribute.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Filter/Filter.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Filter/Html.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Filter/Tag.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Generator/ContentGeneratorInterface.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Generator/FileContentGenerator.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Generator/YoutubeContentGenerator.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Logging/Logger.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Atom.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/DateParser.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Feed.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Item.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/MalformedXmlException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Parser.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/ParserException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/ParserInterface.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss10.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss20.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss91.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss92.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/XmlEntityException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/XmlParser.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/PicoFeedException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ContentFilterProcessor.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ContentGeneratorProcessor.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemPostProcessor.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemProcessorInterface.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ScraperProcessor.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Favicon.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Reader.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/ReaderException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/SubscriptionNotFoundException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/UnsupportedFeedFormatException.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.blog.lemonde.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.blogs.nytimes.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.igen.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.nytimes.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.over-blog.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.phoronix.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.slate.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.theguardian.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wikipedia.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wired.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wsj.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/01net.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/24.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/444.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/888.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/abstrusegoose.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/achgut.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/adventuregamers.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/alainonline.net.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/aljazeera.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allafrica.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allgemeine-zeitung.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/amazingsuperpowers.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/anythingcomic.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ap.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/areadvd.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/arstechnica.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/atv.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/awkwardzombie.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/backchannel.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bangkokpost.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bauerwilli.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bgr.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigfootjustice.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigpicture.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bizjournals.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/biztimes.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bleepingcomputer.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.fefe.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.mapillary.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/brewers.mlb.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buenosairesherald.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bunicomic.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buttersafe.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cad-comic.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chaoslife.findchaos.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chinafile.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cicero.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cliquerefresh.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cnet.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/coinwelt.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/consomac.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cowbirdsinlove.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/crash.net.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/csmonitor.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyjs.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyreporter.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailytech.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/degroupnews.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/derstandard.at.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dilbert.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/discovermagazine.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/distrowatch.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dozodomo.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/drawingboardcomic.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/e-w-e.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/economist.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/encyclopedie.naheulbeuk.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/endlessorigami.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/engadget.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/escapistmagazine.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/espn.go.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/exocomics.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/explosm.net.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/extrafabulouscomics.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/factroom.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcodesign.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcoexist.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcompany.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ffworld.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/foreignpolicy.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fossbytes.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fototelegraf.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fowllanguagecomics.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamechannel.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamestar.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geek.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geektimes.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gerbilwithajetpack.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/giantitp.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/github.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gocomics.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/golem.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gondola.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gorabbit.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/habrahabr.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/happletea.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hardware.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/heise.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hirek.prim.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hotshowlife.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/huffingtonpost.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hvg.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/idokep.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/imogenquest.net.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/index.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/indiehaven.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/inforadio.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ing.dk.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/invisiblebread.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ir.amd.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantimes.co.jp.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantoday.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/journaldugeek.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/jsonline.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/justcoolidea.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kanpai.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/karriere.jobfinder.dk.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kisalfold.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kiszamolo.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kodi.tv.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreaherald.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreatimes.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lastplacecomics.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/legorafi.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lejapon.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lesjoiesducode.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lfg.co.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux-magazin.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linuxinsider.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lists.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loadingartist.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loldwell.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lukesurl.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/macg.co.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maclife.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/magyarkurir.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marc.info.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marriedtothesea.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marycagle.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maximumble.thebookofbiff.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/medium.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mercworks.net.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/metronieuws.nl.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/milwaukeenns.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mno.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mokepon.smackjeeves.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monandroid.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monwindows.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/moya-planeta.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mrlovenstein.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/muckrock.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mynorthshorenow.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nakedCapitalism.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nasa.gov.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nat-geo.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nationaljournal.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nature.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nba.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nedroid.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/networkworld.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/neustadt-ticker.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nextinpact.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/niceteethcomic.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nichtlustig.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nlcafe.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/novo-argumente.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/oglaf.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onhax.net.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onlinekosten.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onmilwaukee.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openculture.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opennet.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openrightsgroup.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opensource.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/optipess.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/origo.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/osnews.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pastebin.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pcgameshardware.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/peebleslab.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/penny-arcade.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pixelbeat.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/plus.google.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/popstrip.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/portfolio.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pro-linux.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publicpolicyforum.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publy.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/putaindecode.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/recode.net.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/retractionwatch.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rockpapershotgun.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rue89.nouvelobs.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rugbyrama.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/salonkolumnisten.com create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/satwcomic.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/science-skeptical.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/scrumalliance.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/securityfocus.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sentfromthemoon.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sitepoint.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/slashdot.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smallhousebliss.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smarthomewelt.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smashingmagazine.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smbc-comics.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/snopes.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/soundandvision.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/spiegel.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stereophile.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stupidfox.net.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/subtraction.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sz.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/takprosto.cc.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/techcrunch.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/the-ebook-reader.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theatlantic.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theawkwardyeti.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thecodinglove.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thedoghousediaries.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thegamercat.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thehindu.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thelocal.se.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themerepublic.net.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themoscowtimes.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thenewslens.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theodd1sout.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theonion.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theregister.co.uk.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thestandard.com.hk.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theverge.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/threepanelsoul.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tichyseinblick.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/timesofindia.indiatimes.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/totalcar.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tozsdeforum.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travel-dealz.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travelo.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treehugger.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treelobsters.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tutorialzine.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twogag.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twokinds.keenspot.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/undeadly.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/upi.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/usatoday.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/version2.dk.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vezess.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vgcats.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vuxml.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/wausaudailyherald.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/welt.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/westfalen-blatt.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bbc.co.uk.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bdgest.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bgr.in.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.businessweek.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.cnn.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.developpez.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.egscomics.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.fakingnews.firstpost.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.forbes.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.franceculture.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.futura-sciences.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.geekculture.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.howtogeek.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lepoint.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lesnumeriques.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.mac4ever.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.makeuseof.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.monsieur-le-chien.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.npr.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.numerama.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.oneindia.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.pseudo-sciences.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.sciencemag.org.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.slate.fr.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.universfreebox.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.zeit.de.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/xkcd.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ymatuhin.ru.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zarojel.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zdnet.com.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zoom.hu.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/CandidateParser.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/ParserInterface.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/RuleLoader.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/RuleParser.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/Scraper.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/Subscription.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionList.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListBuilder.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListParser.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionParser.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomFeedBuilder.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomHelper.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomItemBuilder.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/FeedBuilder.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/ItemBuilder.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20FeedBuilder.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20Helper.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20ItemBuilder.php create mode 100644 user/plugins/admin/vendor/p3k/picofeed/picofeed create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/LICENSE.md create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/README.md create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/bin/pscss create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/composer.json create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/phpcs.xml.dist create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/scss.inc.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Base/Range.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Block.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Cache.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Colors.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/CompilationResult.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Compiler.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Compiler/CachedResult.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Compiler/Environment.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Exception/CompilerException.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Exception/ParserException.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Exception/RangeException.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Exception/SassException.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Exception/SassScriptException.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Exception/ServerException.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Formatter.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compact.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compressed.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Crunched.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Debug.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Expanded.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Nested.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Logger/QuietLogger.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Logger/StreamLogger.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Node.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Node/Number.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/OutputStyle.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Parser.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64VLQ.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Type.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Util.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Util/Path.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/ValueConverter.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Version.php create mode 100644 user/plugins/admin/vendor/scssphp/scssphp/src/Warn.php create mode 100644 user/plugins/advanced-pagecache/CHANGELOG.md create mode 100644 user/plugins/advanced-pagecache/LICENSE create mode 100644 user/plugins/advanced-pagecache/README.md create mode 100644 user/plugins/advanced-pagecache/advanced-pagecache.php create mode 100644 user/plugins/advanced-pagecache/advanced-pagecache.yaml create mode 100644 user/plugins/advanced-pagecache/blueprints.yaml create mode 100644 user/plugins/aura/.gitignore create mode 100644 user/plugins/aura/CHANGELOG.md create mode 100644 user/plugins/aura/LICENSE create mode 100644 user/plugins/aura/README.md create mode 100644 user/plugins/aura/assets/demo-composition-min.png create mode 100644 user/plugins/aura/aura.php create mode 100644 user/plugins/aura/aura.yaml create mode 100644 user/plugins/aura/blueprints.yaml create mode 100644 user/plugins/aura/blueprints/default.yaml create mode 100644 user/plugins/aura/classes/aura.php create mode 100644 user/plugins/aura/classes/image.php create mode 100644 user/plugins/aura/classes/organization.php create mode 100644 user/plugins/aura/classes/person.php create mode 100644 user/plugins/aura/classes/webpage.php create mode 100644 user/plugins/aura/classes/website.php create mode 100644 user/plugins/aura/composer.json create mode 100644 user/plugins/automagic-images/CHANGELOG.md create mode 100644 user/plugins/automagic-images/LICENSE create mode 100644 user/plugins/automagic-images/README.md create mode 100644 user/plugins/automagic-images/adapters/gd.php create mode 100644 user/plugins/automagic-images/adapters/imagick.php create mode 100644 user/plugins/automagic-images/adapters/interface.php create mode 100644 user/plugins/automagic-images/automagic-images.php create mode 100644 user/plugins/automagic-images/automagic-images.yaml create mode 100644 user/plugins/automagic-images/blueprints.yaml create mode 100644 user/plugins/automagic-images/composer.json create mode 100644 user/plugins/automagic-images/composer.lock create mode 100644 user/plugins/automagic-images/composer.phar create mode 100644 user/plugins/automagic-images/languages.yaml create mode 100644 user/plugins/automagic-images/vendor/autoload.php create mode 100644 user/plugins/automagic-images/vendor/composer/ClassLoader.php create mode 100644 user/plugins/automagic-images/vendor/composer/InstalledVersions.php create mode 100644 user/plugins/automagic-images/vendor/composer/LICENSE create mode 100644 user/plugins/automagic-images/vendor/composer/autoload_classmap.php create mode 100644 user/plugins/automagic-images/vendor/composer/autoload_files.php create mode 100644 user/plugins/automagic-images/vendor/composer/autoload_namespaces.php create mode 100644 user/plugins/automagic-images/vendor/composer/autoload_psr4.php create mode 100644 user/plugins/automagic-images/vendor/composer/autoload_real.php create mode 100644 user/plugins/automagic-images/vendor/composer/autoload_static.php create mode 100644 user/plugins/automagic-images/vendor/composer/installed.json create mode 100644 user/plugins/automagic-images/vendor/composer/installed.php create mode 100644 user/plugins/automagic-images/vendor/composer/platform_check.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/CHANGELOG.md create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/LICENSE create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/README.md create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/UPGRADING.md create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/composer.json create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/BodySummarizer.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/BodySummarizerInterface.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Client.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/ClientInterface.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/ClientTrait.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/ClientException.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/ConnectException.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/GuzzleException.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/InvalidArgumentException.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/ServerException.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/TooManyRedirectsException.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/TransferException.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/CurlHandler.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/MockHandler.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/HandlerStack.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/MessageFormatter.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/MessageFormatterInterface.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Middleware.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Pool.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/RedirectMiddleware.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/RequestOptions.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/RetryMiddleware.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/TransferStats.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Utils.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/functions.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/functions_include.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/vendor-bin/php-cs-fixer/composer.json create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/vendor-bin/phpstan/composer.json create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/guzzle/vendor-bin/psalm/composer.json create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/CHANGELOG.md create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/LICENSE create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/Makefile create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/README.md create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/composer.json create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/AggregateException.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/CancellationException.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Coroutine.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Create.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Each.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/EachPromise.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/FulfilledPromise.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Is.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Promise.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/PromiseInterface.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/PromisorInterface.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/RejectedPromise.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/RejectionException.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/TaskQueue.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/TaskQueueInterface.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Utils.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/functions.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/promises/src/functions_include.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/CHANGELOG.md create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/LICENSE create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/README.md create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/composer.json create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/AppendStream.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/BufferStream.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/CachingStream.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/DroppingStream.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/FnStream.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Header.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/InflateStream.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/LazyOpenStream.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/LimitStream.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Message.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/MessageTrait.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/MimeType.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/MultipartStream.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/NoSeekStream.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/PumpStream.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Query.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Request.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Response.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Rfc7230.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/ServerRequest.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Stream.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/StreamWrapper.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/UploadedFile.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Uri.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/UriNormalizer.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/UriResolver.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Utils.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/functions.php create mode 100644 user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/functions_include.php create mode 100644 user/plugins/automagic-images/vendor/myclabs/php-enum/LICENSE create mode 100644 user/plugins/automagic-images/vendor/myclabs/php-enum/README.md create mode 100644 user/plugins/automagic-images/vendor/myclabs/php-enum/SECURITY.md create mode 100644 user/plugins/automagic-images/vendor/myclabs/php-enum/composer.json create mode 100644 user/plugins/automagic-images/vendor/myclabs/php-enum/psalm.xml create mode 100644 user/plugins/automagic-images/vendor/myclabs/php-enum/src/Enum.php create mode 100644 user/plugins/automagic-images/vendor/myclabs/php-enum/src/PHPUnit/Comparator.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/SECURITY.md create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/composer.json create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Content.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Contracts/Dom/CleanerInterface.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Contracts/Dom/ParserInterface.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Contracts/DomInterface.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Contracts/Selector/ParserInterface.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Contracts/Selector/SeekerInterface.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Contracts/Selector/SelectorInterface.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Selector/ParsedSelectorCollectionDTO.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Selector/ParsedSelectorDTO.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Selector/RuleDTO.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Tag/AttributeDTO.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/TagDTO.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Discovery/CleanerDiscovery.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Discovery/DomParserDiscovery.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Discovery/SeekerDiscovery.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Discovery/SelectorParserDiscovery.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Cleaner.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Node/AbstractNode.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Node/ArrayNode.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Node/Collection.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Node/HtmlNode.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Node/InnerNode.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Node/LeafNode.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Node/TextNode.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Parser.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/RootAccessTrait.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Tag.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Enum/StringToken.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Exceptions/ChildNotFoundException.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Exceptions/CircularException.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Exceptions/ContentLengthException.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Exceptions/CurlException.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Exceptions/EmptyCollectionException.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Exceptions/LogicalException.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Exceptions/NotLoadedException.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Exceptions/ParentNotFoundException.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Exceptions/StrictException.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Exceptions/Tag/AttributeNotFoundException.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Exceptions/UnknownChildTypeException.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Exceptions/UnknownOptionException.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Finder.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Options.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Selector/Parser.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Selector/Seeker.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Selector/Selector.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/StaticDom.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/string-encode/CONTRIBUTING.md create mode 100644 user/plugins/automagic-images/vendor/paquettg/string-encode/README.md create mode 100644 user/plugins/automagic-images/vendor/paquettg/string-encode/composer.json create mode 100644 user/plugins/automagic-images/vendor/paquettg/string-encode/phpunit.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/string-encode/phpunit.xml create mode 100644 user/plugins/automagic-images/vendor/paquettg/string-encode/src/stringEncode/Encode.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/string-encode/src/stringEncode/Exception.php create mode 100644 user/plugins/automagic-images/vendor/paquettg/string-encode/tests/EncodeTest.php create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/CHANGELOG.md create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/LICENSE create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/README.md create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/composer.json create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/puli.json create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/src/Exception.php create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/src/Exception/HttpException.php create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/src/Exception/NetworkException.php create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/src/Exception/RequestAwareTrait.php create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/src/Exception/RequestException.php create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/src/Exception/TransferException.php create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/src/HttpAsyncClient.php create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/src/HttpClient.php create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/src/Promise/HttpFulfilledPromise.php create mode 100644 user/plugins/automagic-images/vendor/php-http/httplug/src/Promise/HttpRejectedPromise.php create mode 100644 user/plugins/automagic-images/vendor/php-http/promise/CHANGELOG.md create mode 100644 user/plugins/automagic-images/vendor/php-http/promise/LICENSE create mode 100644 user/plugins/automagic-images/vendor/php-http/promise/README.md create mode 100644 user/plugins/automagic-images/vendor/php-http/promise/composer.json create mode 100644 user/plugins/automagic-images/vendor/php-http/promise/src/FulfilledPromise.php create mode 100644 user/plugins/automagic-images/vendor/php-http/promise/src/Promise.php create mode 100644 user/plugins/automagic-images/vendor/php-http/promise/src/RejectedPromise.php create mode 100644 user/plugins/automagic-images/vendor/psr/http-client/CHANGELOG.md create mode 100644 user/plugins/automagic-images/vendor/psr/http-client/LICENSE create mode 100644 user/plugins/automagic-images/vendor/psr/http-client/README.md create mode 100644 user/plugins/automagic-images/vendor/psr/http-client/composer.json create mode 100644 user/plugins/automagic-images/vendor/psr/http-client/src/ClientExceptionInterface.php create mode 100644 user/plugins/automagic-images/vendor/psr/http-client/src/ClientInterface.php create mode 100644 user/plugins/automagic-images/vendor/psr/http-client/src/NetworkExceptionInterface.php create mode 100644 user/plugins/automagic-images/vendor/psr/http-client/src/RequestExceptionInterface.php create mode 100644 user/plugins/automagic-images/vendor/psr/http-message/CHANGELOG.md create mode 100644 user/plugins/automagic-images/vendor/psr/http-message/LICENSE create mode 100644 user/plugins/automagic-images/vendor/psr/http-message/README.md create mode 100644 user/plugins/automagic-images/vendor/psr/http-message/composer.json create mode 100644 user/plugins/automagic-images/vendor/psr/http-message/src/MessageInterface.php create mode 100644 user/plugins/automagic-images/vendor/psr/http-message/src/RequestInterface.php create mode 100644 user/plugins/automagic-images/vendor/psr/http-message/src/ResponseInterface.php create mode 100644 user/plugins/automagic-images/vendor/psr/http-message/src/ServerRequestInterface.php create mode 100644 user/plugins/automagic-images/vendor/psr/http-message/src/StreamInterface.php create mode 100644 user/plugins/automagic-images/vendor/psr/http-message/src/UploadedFileInterface.php create mode 100644 user/plugins/automagic-images/vendor/psr/http-message/src/UriInterface.php create mode 100644 user/plugins/automagic-images/vendor/ralouphie/getallheaders/LICENSE create mode 100644 user/plugins/automagic-images/vendor/ralouphie/getallheaders/README.md create mode 100644 user/plugins/automagic-images/vendor/ralouphie/getallheaders/composer.json create mode 100644 user/plugins/automagic-images/vendor/ralouphie/getallheaders/src/getallheaders.php create mode 100644 user/plugins/devtools/CHANGELOG.md create mode 100644 user/plugins/devtools/LICENSE create mode 100644 user/plugins/devtools/README.md create mode 100644 user/plugins/devtools/blueprints.yaml create mode 100644 user/plugins/devtools/classes/DevToolsCommand.php create mode 100644 user/plugins/devtools/cli/NewBlueprintCommand.php create mode 100644 user/plugins/devtools/cli/NewPluginCommand.php create mode 100644 user/plugins/devtools/cli/NewThemeCommand.php create mode 100644 user/plugins/devtools/components/blueprint/append/blueprint.yaml.twig create mode 100644 user/plugins/devtools/components/blueprint/newtab/blueprint.yaml.twig create mode 100644 user/plugins/devtools/components/plugin/blank/CHANGELOG.md.twig create mode 100644 user/plugins/devtools/components/plugin/blank/LICENSE.twig create mode 100644 user/plugins/devtools/components/plugin/blank/README.md.twig create mode 100644 user/plugins/devtools/components/plugin/blank/blueprints.yaml.twig create mode 100644 user/plugins/devtools/components/plugin/blank/classes/.gitkeep create mode 100644 user/plugins/devtools/components/plugin/blank/composer.json.twig create mode 100644 user/plugins/devtools/components/plugin/blank/languages.yaml.twig create mode 100644 user/plugins/devtools/components/plugin/blank/plugin.php.twig create mode 100644 user/plugins/devtools/components/plugin/blank/plugin.yaml.twig create mode 100644 user/plugins/devtools/components/theme/inheritance/CHANGELOG.md.twig create mode 100644 user/plugins/devtools/components/theme/inheritance/LICENSE.twig create mode 100644 user/plugins/devtools/components/theme/inheritance/README.md.twig create mode 100644 user/plugins/devtools/components/theme/inheritance/blueprints.yaml.twig create mode 100644 user/plugins/devtools/components/theme/inheritance/css/.gitkeep create mode 100644 user/plugins/devtools/components/theme/inheritance/js/.gitkeep create mode 100644 user/plugins/devtools/components/theme/inheritance/screenshot.jpg create mode 100644 user/plugins/devtools/components/theme/inheritance/templates/.gitkeep create mode 100644 user/plugins/devtools/components/theme/inheritance/theme.php.twig create mode 100644 user/plugins/devtools/components/theme/inheritance/theme.yaml.twig create mode 100644 user/plugins/devtools/components/theme/inheritance/thumbnail.jpg create mode 100644 user/plugins/devtools/components/theme/pure-blank/CHANGELOG.md.twig create mode 100644 user/plugins/devtools/components/theme/pure-blank/LICENSE.twig create mode 100644 user/plugins/devtools/components/theme/pure-blank/README.md.twig create mode 100644 user/plugins/devtools/components/theme/pure-blank/blueprints.yaml.twig create mode 100644 user/plugins/devtools/components/theme/pure-blank/css/custom.css create mode 100644 user/plugins/devtools/components/theme/pure-blank/fonts/.gitkeep create mode 100644 user/plugins/devtools/components/theme/pure-blank/images/logo.png create mode 100644 user/plugins/devtools/components/theme/pure-blank/js/.gitkeep create mode 100644 user/plugins/devtools/components/theme/pure-blank/screenshot.jpg create mode 100644 user/plugins/devtools/components/theme/pure-blank/templates/default.html.twig create mode 100644 user/plugins/devtools/components/theme/pure-blank/templates/error.html.twig create mode 100644 user/plugins/devtools/components/theme/pure-blank/templates/partials/base.html.twig create mode 100644 user/plugins/devtools/components/theme/pure-blank/templates/partials/navigation.html.twig create mode 100644 user/plugins/devtools/components/theme/pure-blank/theme.php.twig create mode 100644 user/plugins/devtools/components/theme/pure-blank/theme.yaml.twig create mode 100644 user/plugins/devtools/components/theme/pure-blank/thumbnail.jpg create mode 100644 user/plugins/devtools/components/theme/tailwind/CHANGELOG.md.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/LICENSE.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/README.md.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/blueprints.yaml.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/css/site.css create mode 100644 user/plugins/devtools/components/theme/tailwind/fonts/.gitkeep create mode 100644 user/plugins/devtools/components/theme/tailwind/images/logo.png create mode 100644 user/plugins/devtools/components/theme/tailwind/js/.gitkeep create mode 100644 user/plugins/devtools/components/theme/tailwind/languages/en.yaml.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/package.json.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/postcss.config.js create mode 100644 user/plugins/devtools/components/theme/tailwind/screenshot.jpg create mode 100644 user/plugins/devtools/components/theme/tailwind/tailwind.config.js.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/templates/default.html.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/templates/error.html.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/templates/partials/base.html.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/templates/partials/navigation.html.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/theme.php.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/theme.yaml.twig create mode 100644 user/plugins/devtools/components/theme/tailwind/thumbnail.jpg create mode 100644 user/plugins/devtools/devtools.php create mode 100644 user/plugins/devtools/devtools.yaml create mode 100644 user/plugins/devtools/languages.yaml create mode 100644 user/plugins/email/CHANGELOG.md create mode 100644 user/plugins/email/LICENSE create mode 100644 user/plugins/email/README.md create mode 100644 user/plugins/email/blueprints.yaml create mode 100644 user/plugins/email/classes/Email.php create mode 100644 user/plugins/email/classes/Utils.php create mode 100644 user/plugins/email/cli/ClearQueueFailuresCommand.php create mode 100644 user/plugins/email/cli/FlushQueueCommand.php create mode 100644 user/plugins/email/cli/TestEmailCommand.php create mode 100644 user/plugins/email/composer.json create mode 100644 user/plugins/email/composer.lock create mode 100644 user/plugins/email/email.php create mode 100644 user/plugins/email/email.yaml create mode 100644 user/plugins/email/hebe.json create mode 100644 user/plugins/email/languages.yaml create mode 100644 user/plugins/email/templates/email/base.html.twig create mode 100644 user/plugins/email/vendor/autoload.php create mode 100644 user/plugins/email/vendor/composer/ClassLoader.php create mode 100644 user/plugins/email/vendor/composer/InstalledVersions.php create mode 100644 user/plugins/email/vendor/composer/LICENSE create mode 100644 user/plugins/email/vendor/composer/autoload_classmap.php create mode 100644 user/plugins/email/vendor/composer/autoload_files.php create mode 100644 user/plugins/email/vendor/composer/autoload_namespaces.php create mode 100644 user/plugins/email/vendor/composer/autoload_psr4.php create mode 100644 user/plugins/email/vendor/composer/autoload_real.php create mode 100644 user/plugins/email/vendor/composer/autoload_static.php create mode 100644 user/plugins/email/vendor/composer/installed.json create mode 100644 user/plugins/email/vendor/composer/installed.php create mode 100644 user/plugins/email/vendor/composer/platform_check.php create mode 100644 user/plugins/email/vendor/doctrine/lexer/LICENSE create mode 100644 user/plugins/email/vendor/doctrine/lexer/README.md create mode 100644 user/plugins/email/vendor/doctrine/lexer/composer.json create mode 100644 user/plugins/email/vendor/doctrine/lexer/lib/Doctrine/Common/Lexer/AbstractLexer.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/LICENSE create mode 100644 user/plugins/email/vendor/egulias/email-validator/composer.json create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/EmailLexer.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/EmailParser.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/EmailValidator.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/AtextAfterCFWS.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/CRLFAtTheEnd.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/CRLFX2.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/CRNoLF.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/CharNotAllowed.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/CommaInDomain.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/ConsecutiveAt.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/ConsecutiveDot.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/DomainAcceptsNoMail.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/DomainHyphened.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/DotAtEnd.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/DotAtStart.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/ExpectingAT.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/ExpectingATEXT.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/ExpectingCTEXT.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/ExpectingDTEXT.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/ExpectingDomainLiteralClose.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/ExpectingQPair.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/InvalidEmail.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/LocalOrReservedDomain.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/NoDNSRecord.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/NoDomainPart.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/NoLocalPart.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/UnclosedComment.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/UnclosedQuotedString.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Exception/UnopenedComment.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Parser/DomainPart.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Parser/LocalPart.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Parser/Parser.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Validation/DNSCheckValidation.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Validation/EmailValidation.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Validation/Error/RFCWarnings.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Validation/Error/SpoofEmail.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Validation/Exception/EmptyValidationList.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Validation/MultipleErrors.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Validation/RFCValidation.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Validation/SpoofCheckValidation.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/AddressLiteral.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/CFWSNearAt.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/CFWSWithFWS.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/Comment.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/DeprecatedComment.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/DomainLiteral.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/DomainTooLong.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/EmailTooLong.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6BadChar.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6ColonEnd.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6ColonStart.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6Deprecated.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6DoubleColon.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6GroupCount.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/IPV6MaxGroups.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/LabelTooLong.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/LocalTooLong.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/NoDNSMXRecord.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/ObsoleteDTEXT.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/QuotedPart.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/QuotedString.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/TLD.php create mode 100644 user/plugins/email/vendor/egulias/email-validator/src/Warning/Warning.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/.gitattributes create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/.github/ISSUE_TEMPLATE.md create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/.gitignore create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/.php_cs.dist create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/.travis.yml create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/CHANGES create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/LICENSE create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/README.md create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/composer.json create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/doc/headers.rst create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/doc/index.rst create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/doc/introduction.rst create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/doc/japanese.rst create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/doc/messages.rst create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/doc/plugins.rst create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/doc/sending.rst create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/AddressEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/AddressEncoder/IdnAddressEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/AddressEncoder/Utf8AddressEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/AddressEncoderException.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Attachment.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/AbstractFilterableInputStream.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/ArrayByteStream.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/FileByteStream.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/TemporaryFileByteStream.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterReader.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterReader/GenericFixedWidthReader.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterReader/UsAsciiReader.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterReader/Utf8Reader.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterReaderFactory.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterReaderFactory/SimpleCharacterReaderFactory.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterStream.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterStream/ArrayCharacterStream.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterStream/NgCharacterStream.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/ConfigurableSpool.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/DependencyContainer.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/DependencyException.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/EmbeddedFile.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Encoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Encoder/Base64Encoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Encoder/QpEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Encoder/Rfc2231Encoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/CommandEvent.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/CommandListener.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/Event.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/EventDispatcher.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/EventListener.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/EventObject.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/ResponseEvent.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/ResponseListener.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/SendEvent.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/SendListener.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/SimpleEventDispatcher.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/TransportChangeEvent.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/TransportChangeListener.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/TransportExceptionEvent.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/TransportExceptionListener.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/FailoverTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/FileSpool.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/FileStream.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Filterable.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/IdGenerator.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Image.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/InputByteStream.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/IoException.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/ArrayKeyCache.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/KeyCacheInputStream.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/NullKeyCache.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/SimpleKeyCacheInputStream.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/LoadBalancedTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mailer.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mailer/ArrayRecipientIterator.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mailer/RecipientIterator.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/MemorySpool.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Message.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Attachment.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/CharsetObserver.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/Base64ContentEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/NativeQpContentEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/NullContentEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/PlainContentEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/QpContentEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/QpContentEncoderProxy.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/RawContentEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/EmbeddedFile.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/EncodingObserver.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Header.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/HeaderEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/HeaderEncoder/Base64HeaderEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/HeaderEncoder/QpHeaderEncoder.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/AbstractHeader.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/DateHeader.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/IdentificationHeader.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/MailboxHeader.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/OpenDKIMHeader.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/ParameterizedHeader.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/PathHeader.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/UnstructuredHeader.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/IdGenerator.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/MimePart.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/SimpleHeaderFactory.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/SimpleHeaderSet.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/SimpleMessage.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/SimpleMimeEntity.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/MimePart.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/NullTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/OutputByteStream.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/AntiFloodPlugin.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/BandwidthMonitorPlugin.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/Decorator/Replacements.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/DecoratorPlugin.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/ImpersonatePlugin.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/Logger.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/LoggerPlugin.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/Loggers/ArrayLogger.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/Loggers/EchoLogger.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/MessageLogger.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/Pop/Pop3Connection.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/Pop/Pop3Exception.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/PopBeforeSmtpPlugin.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/RedirectingPlugin.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/Reporter.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/ReporterPlugin.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/Reporters/HitReporter.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/Reporters/HtmlReporter.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/Sleeper.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/ThrottlerPlugin.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/Timer.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Preferences.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/ReplacementFilterFactory.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/RfcComplianceException.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/SendmailTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Signer.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Signers/BodySigner.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Signers/DKIMSigner.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Signers/DomainKeySigner.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Signers/HeaderSigner.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Signers/OpenDKIMSigner.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Signers/SMimeSigner.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/SmtpTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Spool.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/SpoolTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/StreamFilter.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/StreamFilters/ByteArrayReplacementFilter.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/StreamFilters/StringReplacementFilter.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/StreamFilters/StringReplacementFilterFactory.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/SwiftException.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/AbstractSmtpTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Auth/CramMd5Authenticator.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Auth/LoginAuthenticator.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Auth/NTLMAuthenticator.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Auth/PlainAuthenticator.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Auth/XOAuth2Authenticator.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/AuthHandler.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Authenticator.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/EightBitMimeHandler.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/SmtpUtf8Handler.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/EsmtpHandler.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/EsmtpTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/FailoverTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/IoBuffer.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/LoadBalancedTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/NullTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/SendmailTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/SmtpAgent.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/SpoolTransport.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/classes/Swift/TransportException.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/dependency_maps/cache_deps.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/dependency_maps/message_deps.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/dependency_maps/mime_deps.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/dependency_maps/transport_deps.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/mime_types.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/preferences.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/swift_required.php create mode 100644 user/plugins/email/vendor/swiftmailer/swiftmailer/lib/swiftmailer_generate_mimes_config.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Iconv.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/LICENSE create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/README.md create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.big5.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp037.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp1006.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp1026.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp424.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp437.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp500.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp737.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp775.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp850.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp852.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp855.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp856.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp857.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp860.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp861.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp862.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp863.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp864.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp865.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp866.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp869.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp874.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp875.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp932.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp936.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp949.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.cp950.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-1.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-10.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-11.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-13.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-14.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-15.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-16.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-2.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-3.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-4.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-5.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-6.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-7.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-8.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.iso-8859-9.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.koi8-r.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.koi8-u.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.us-ascii.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.windows-1250.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.windows-1251.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.windows-1252.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.windows-1253.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.windows-1254.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.windows-1255.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.windows-1256.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.windows-1257.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/from.windows-1258.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/Resources/charset/translit.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/bootstrap.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/bootstrap80.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-iconv/composer.json create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/Idn.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/Info.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/LICENSE create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/README.md create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/Regex.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/deviation.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_mapped.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_valid.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/ignored.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/mapped.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/Resources/unidata/virama.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/bootstrap.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/bootstrap80.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-idn/composer.json create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-normalizer/LICENSE create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-normalizer/Normalizer.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-normalizer/README.md create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalComposition.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-normalizer/bootstrap.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-normalizer/bootstrap80.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-intl-normalizer/composer.json create mode 100644 user/plugins/email/vendor/symfony/polyfill-mbstring/LICENSE create mode 100644 user/plugins/email/vendor/symfony/polyfill-mbstring/Mbstring.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-mbstring/README.md create mode 100644 user/plugins/email/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-mbstring/bootstrap.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-mbstring/bootstrap80.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-mbstring/composer.json create mode 100644 user/plugins/email/vendor/symfony/polyfill-php72/LICENSE create mode 100644 user/plugins/email/vendor/symfony/polyfill-php72/Php72.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-php72/README.md create mode 100644 user/plugins/email/vendor/symfony/polyfill-php72/bootstrap.php create mode 100644 user/plugins/email/vendor/symfony/polyfill-php72/composer.json create mode 100644 user/plugins/flex-objects/.eslintrc create mode 100644 user/plugins/flex-objects/.gitignore create mode 100644 user/plugins/flex-objects/CHANGELOG.md create mode 100644 user/plugins/flex-objects/LICENSE create mode 100644 user/plugins/flex-objects/README.md create mode 100644 user/plugins/flex-objects/admin/pages/flex-objects.md create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects.json.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/layouts/404.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/layouts/accounts/partials/top.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/add.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/back.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/configuration.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/delete.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export-csv.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/languages.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview-open.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/save.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/configure.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/edit.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/list.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list_actions.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/modals/remove.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/preview.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/configure.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/edit.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/list.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/preview.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/types.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/default/types.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/add.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/back.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/copy.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/delete.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/move.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-child.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-next.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-parent.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-prev.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/preview.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/save.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/edit.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/list.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/list/columns.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/list/list.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/pages/preview.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/configure.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/edit.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/list.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/configure.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/list.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/forms/fields/flex-objects/flex-objects.html.twig create mode 100644 user/plugins/flex-objects/admin/templates/forms/fields/save-redirect/save-redirect.html.twig create mode 100644 user/plugins/flex-objects/app/columns/finder.js create mode 100644 user/plugins/flex-objects/app/columns/index.js create mode 100644 user/plugins/flex-objects/app/filters/index.js create mode 100644 user/plugins/flex-objects/app/filters/panel.js create mode 100644 user/plugins/flex-objects/app/list/App.vue create mode 100644 user/plugins/flex-objects/app/list/VuetableCssConfig.js create mode 100644 user/plugins/flex-objects/app/list/components/ContentLoader.vue create mode 100644 user/plugins/flex-objects/app/list/components/FilterBar.vue create mode 100644 user/plugins/flex-objects/app/list/components/Table.vue create mode 100644 user/plugins/flex-objects/app/list/index.js create mode 100644 user/plugins/flex-objects/app/main.js create mode 100644 user/plugins/flex-objects/app/utils/finder.js create mode 100644 user/plugins/flex-objects/app/utils/get-filters.js create mode 100644 user/plugins/flex-objects/app/utils/indeterminate.js create mode 100644 user/plugins/flex-objects/assets/flex-objects-directory.png create mode 100644 user/plugins/flex-objects/assets/flex-objects-edit.png create mode 100644 user/plugins/flex-objects/assets/flex-objects-list.png create mode 100644 user/plugins/flex-objects/assets/flex-objects-options.png create mode 100644 user/plugins/flex-objects/assets/flex-objects-site.png create mode 100644 user/plugins/flex-objects/blueprints.yaml create mode 100644 user/plugins/flex-objects/blueprints/flex-objects.yaml create mode 100644 user/plugins/flex-objects/blueprints/flex-objects/contacts.yaml create mode 100644 user/plugins/flex-objects/blueprints/flex-objects/pages.yaml create mode 100644 user/plugins/flex-objects/blueprints/flex-objects/user-accounts.yaml create mode 100644 user/plugins/flex-objects/blueprints/flex-objects/user-groups.yaml create mode 100644 user/plugins/flex-objects/blueprints/pages/flex-objects.yaml create mode 100644 user/plugins/flex-objects/classes/Admin/AdminController.php create mode 100644 user/plugins/flex-objects/classes/Controllers/AbstractController.php create mode 100644 user/plugins/flex-objects/classes/Controllers/MediaController.php create mode 100644 user/plugins/flex-objects/classes/Controllers/ObjectController.php create mode 100644 user/plugins/flex-objects/classes/Flex.php create mode 100644 user/plugins/flex-objects/classes/FlexFormFactory.php create mode 100644 user/plugins/flex-objects/classes/FlexRouter.php create mode 100644 user/plugins/flex-objects/classes/Table/DataTable.php create mode 100644 user/plugins/flex-objects/cli/FlexConvertDataCommand.php create mode 100644 user/plugins/flex-objects/composer.json create mode 100644 user/plugins/flex-objects/composer.lock create mode 100644 user/plugins/flex-objects/css/admin.css create mode 100644 user/plugins/flex-objects/css/admin.css.map create mode 100644 user/plugins/flex-objects/css/admin.min.css create mode 100644 user/plugins/flex-objects/css/site.css create mode 100644 user/plugins/flex-objects/css/site.css.map create mode 100644 user/plugins/flex-objects/css/site.min.css create mode 100644 user/plugins/flex-objects/data/flex-objects/contacts.json create mode 100644 user/plugins/flex-objects/flex-objects.php create mode 100644 user/plugins/flex-objects/flex-objects.yaml create mode 100644 user/plugins/flex-objects/js/flex-objects.js create mode 100644 user/plugins/flex-objects/js/list.min.js create mode 100644 user/plugins/flex-objects/languages/de.yaml create mode 100644 user/plugins/flex-objects/languages/en.yaml create mode 100644 user/plugins/flex-objects/languages/ru.yaml create mode 100644 user/plugins/flex-objects/package.json create mode 100644 user/plugins/flex-objects/permissions.yaml create mode 100644 user/plugins/flex-objects/scss/_preset.scss create mode 100644 user/plugins/flex-objects/scss/admin.scss create mode 100644 user/plugins/flex-objects/scss/plugin/_admin.scss create mode 100644 user/plugins/flex-objects/scss/plugin/_site.scss create mode 100644 user/plugins/flex-objects/scss/site.scss create mode 100644 user/plugins/flex-objects/templates/flex-edit.html.twig create mode 100644 user/plugins/flex-objects/templates/flex-objects.html.twig create mode 100644 user/plugins/flex-objects/templates/flex-objects/directories/default.html.twig create mode 100644 user/plugins/flex-objects/templates/flex-objects/page.html.twig create mode 100644 user/plugins/flex-objects/templates/flex-objects/views/404.html.twig create mode 100644 user/plugins/flex-objects/templates/flex-objects/views/directories.html.twig create mode 100644 user/plugins/flex-objects/templates/flex-objects/views/directory.html.twig create mode 100644 user/plugins/flex-objects/templates/flex-objects/views/object.html.twig create mode 100644 user/plugins/flex-objects/templates/flex/contacts/collection/default.html.twig create mode 100644 user/plugins/flex-objects/templates/flex/contacts/object/default.html.twig create mode 100644 user/plugins/flex-objects/templates/flex/contacts/object/list-default.html.twig create mode 100644 user/plugins/flex-objects/templates/flex/user-accounts/collection/default.html.twig create mode 100644 user/plugins/flex-objects/templates/flex/user-accounts/object/default.html.twig create mode 100644 user/plugins/flex-objects/templates/flex/user-accounts/object/list-default.html.twig create mode 100644 user/plugins/flex-objects/templates/flex/user-groups/collection/default.html.twig create mode 100644 user/plugins/flex-objects/templates/flex/user-groups/object/default.html.twig create mode 100644 user/plugins/flex-objects/templates/flex/user-groups/object/list-default.html.twig create mode 100644 user/plugins/flex-objects/templates/forms/fields/filepicker/filepicker.html.twig create mode 100644 user/plugins/flex-objects/templates/forms/fields/list/list.html.twig create mode 100644 user/plugins/flex-objects/templates/forms/fields/pagemedia/pagemedia.html.twig create mode 100644 user/plugins/flex-objects/templates/forms/fields/pagemedia/template.html.twig create mode 100644 user/plugins/flex-objects/vendor/autoload.php create mode 100644 user/plugins/flex-objects/vendor/composer/ClassLoader.php create mode 100644 user/plugins/flex-objects/vendor/composer/InstalledVersions.php create mode 100644 user/plugins/flex-objects/vendor/composer/LICENSE create mode 100644 user/plugins/flex-objects/vendor/composer/autoload_classmap.php create mode 100644 user/plugins/flex-objects/vendor/composer/autoload_namespaces.php create mode 100644 user/plugins/flex-objects/vendor/composer/autoload_psr4.php create mode 100644 user/plugins/flex-objects/vendor/composer/autoload_real.php create mode 100644 user/plugins/flex-objects/vendor/composer/autoload_static.php create mode 100644 user/plugins/flex-objects/vendor/composer/installed.json create mode 100644 user/plugins/flex-objects/vendor/composer/installed.php create mode 100644 user/plugins/flex-objects/vendor/composer/platform_check.php create mode 100644 user/plugins/flex-objects/watch.sh create mode 100644 user/plugins/flex-objects/webpack.conf.js create mode 100644 user/plugins/flex-objects/yarn.lock create mode 100644 user/plugins/form/.eslintrc create mode 100644 user/plugins/form/.gitignore create mode 100644 user/plugins/form/CHANGELOG.md create mode 100644 user/plugins/form/LICENSE create mode 100644 user/plugins/form/README.md create mode 100644 user/plugins/form/app/fields/array.js create mode 100644 user/plugins/form/app/fields/file.js create mode 100644 user/plugins/form/app/fields/form.js create mode 100644 user/plugins/form/app/fields/index.js create mode 100644 user/plugins/form/app/fields/media.js create mode 100644 user/plugins/form/app/fields/tabs.js create mode 100644 user/plugins/form/app/main.js create mode 100644 user/plugins/form/app/utils/keep-alive.js create mode 100644 user/plugins/form/assets/dropzone.min.css create mode 100644 user/plugins/form/assets/form-styles.css create mode 100644 user/plugins/form/assets/form-styles.min.css create mode 100644 user/plugins/form/assets/form.min.js create mode 100644 user/plugins/form/assets/form.vendor.js create mode 100644 user/plugins/form/assets/object.assign.polyfill.js create mode 100644 user/plugins/form/assets/signature_pad.js create mode 100644 user/plugins/form/blueprints.yaml create mode 100644 user/plugins/form/classes/Form.php create mode 100644 user/plugins/form/classes/FormFactory.php create mode 100644 user/plugins/form/classes/Forms.php create mode 100644 user/plugins/form/classes/TwigExtension.php create mode 100644 user/plugins/form/composer.json create mode 100644 user/plugins/form/composer.lock create mode 100644 user/plugins/form/form.php create mode 100644 user/plugins/form/form.yaml create mode 100644 user/plugins/form/gulpfile.js create mode 100644 user/plugins/form/hebe.json create mode 100644 user/plugins/form/languages.yaml create mode 100644 user/plugins/form/package.json create mode 100644 user/plugins/form/scss/form-styles.scss create mode 100644 user/plugins/form/templates/form-messages.html.twig create mode 100644 user/plugins/form/templates/form-messages.json.twig create mode 100644 user/plugins/form/templates/form.html.twig create mode 100644 user/plugins/form/templates/form.json.twig create mode 100644 user/plugins/form/templates/formdata.html.twig create mode 100644 user/plugins/form/templates/forms/ajax.json.twig create mode 100644 user/plugins/form/templates/forms/data.html.twig create mode 100644 user/plugins/form/templates/forms/data.txt.twig create mode 100644 user/plugins/form/templates/forms/default/data.html.twig create mode 100644 user/plugins/form/templates/forms/default/data.txt.twig create mode 100644 user/plugins/form/templates/forms/default/field.html.twig create mode 100644 user/plugins/form/templates/forms/default/fields.html.twig create mode 100644 user/plugins/form/templates/forms/default/form.html.twig create mode 100644 user/plugins/form/templates/forms/default/toggleable.html.twig create mode 100644 user/plugins/form/templates/forms/dropzone/template.html.twig create mode 100644 user/plugins/form/templates/forms/field.html.twig create mode 100644 user/plugins/form/templates/forms/fields/array/array.html.twig create mode 100644 user/plugins/form/templates/forms/fields/avatar/avatar.html.twig create mode 100644 user/plugins/form/templates/forms/fields/captcha/captcha.html.twig create mode 100644 user/plugins/form/templates/forms/fields/checkbox/checkbox.html.twig create mode 100644 user/plugins/form/templates/forms/fields/checkboxes/checkboxes.html.twig create mode 100644 user/plugins/form/templates/forms/fields/color/color.html.twig create mode 100644 user/plugins/form/templates/forms/fields/column/column.html.twig create mode 100644 user/plugins/form/templates/forms/fields/columns/columns.html.twig create mode 100644 user/plugins/form/templates/forms/fields/conditional/conditional.html.twig create mode 100644 user/plugins/form/templates/forms/fields/date/date.html.twig create mode 100644 user/plugins/form/templates/forms/fields/date/edit_list.html.twig create mode 100644 user/plugins/form/templates/forms/fields/datetime/datetime.html.twig create mode 100644 user/plugins/form/templates/forms/fields/datetime/edit_list.html.twig create mode 100644 user/plugins/form/templates/forms/fields/display/display.html.twig create mode 100644 user/plugins/form/templates/forms/fields/email/email.html.twig create mode 100644 user/plugins/form/templates/forms/fields/fieldset/fieldset.html.twig create mode 100644 user/plugins/form/templates/forms/fields/file/file.html.twig create mode 100644 user/plugins/form/templates/forms/fields/formname/formname.html.twig create mode 100644 user/plugins/form/templates/forms/fields/formtask/formtask.html.twig create mode 100644 user/plugins/form/templates/forms/fields/hidden/hidden.html.twig create mode 100644 user/plugins/form/templates/forms/fields/honeypot/honeypot.html.twig create mode 100644 user/plugins/form/templates/forms/fields/ignore/ignore.html.twig create mode 100644 user/plugins/form/templates/forms/fields/key/key.html.twig create mode 100644 user/plugins/form/templates/forms/fields/month/month.html.twig create mode 100644 user/plugins/form/templates/forms/fields/nonce/nonce.html.twig create mode 100644 user/plugins/form/templates/forms/fields/number/number.html.twig create mode 100644 user/plugins/form/templates/forms/fields/password/password.html.twig create mode 100644 user/plugins/form/templates/forms/fields/radio/radio.html.twig create mode 100644 user/plugins/form/templates/forms/fields/range/range.html.twig create mode 100644 user/plugins/form/templates/forms/fields/section/section.html.twig create mode 100644 user/plugins/form/templates/forms/fields/select/select.html.twig create mode 100644 user/plugins/form/templates/forms/fields/select_optgroup/select_optgroup.html.twig create mode 100644 user/plugins/form/templates/forms/fields/signature/signature.html.twig create mode 100644 user/plugins/form/templates/forms/fields/spacer/spacer.html.twig create mode 100644 user/plugins/form/templates/forms/fields/switch/switch.html.twig create mode 100644 user/plugins/form/templates/forms/fields/tab/tab.html.twig create mode 100644 user/plugins/form/templates/forms/fields/tabs/tabs.html.twig create mode 100644 user/plugins/form/templates/forms/fields/tel/tel.html.twig create mode 100644 user/plugins/form/templates/forms/fields/text/edit_list.html.twig create mode 100644 user/plugins/form/templates/forms/fields/text/text.html.twig create mode 100644 user/plugins/form/templates/forms/fields/textarea/textarea.html.twig create mode 100644 user/plugins/form/templates/forms/fields/time/edit_list.html.twig create mode 100644 user/plugins/form/templates/forms/fields/time/time.html.twig create mode 100644 user/plugins/form/templates/forms/fields/toggle/edit_list.html.twig create mode 100644 user/plugins/form/templates/forms/fields/toggle/toggle.html.twig create mode 100644 user/plugins/form/templates/forms/fields/uniqueid/uniqueid.html.twig create mode 100644 user/plugins/form/templates/forms/fields/url/edit_list.html.twig create mode 100644 user/plugins/form/templates/forms/fields/url/url.html.twig create mode 100644 user/plugins/form/templates/forms/fields/value/value.html.twig create mode 100644 user/plugins/form/templates/forms/fields/week/week.html.twig create mode 100644 user/plugins/form/templates/forms/form.html.twig create mode 100644 user/plugins/form/templates/forms/layouts/button.html.twig create mode 100644 user/plugins/form/templates/forms/layouts/button/default-button.html.twig create mode 100644 user/plugins/form/templates/forms/layouts/field-variables.html.twig create mode 100644 user/plugins/form/templates/forms/layouts/field.html.twig create mode 100644 user/plugins/form/templates/forms/layouts/field/default-field.html.twig create mode 100644 user/plugins/form/templates/forms/layouts/form.html.twig create mode 100644 user/plugins/form/templates/forms/layouts/form/default-form.html.twig create mode 100644 user/plugins/form/templates/modular/form.html.twig create mode 100644 user/plugins/form/templates/partials/form-messages.html.twig create mode 100644 user/plugins/form/templates/partials/form-messages.json.twig create mode 100644 user/plugins/form/vendor/autoload.php create mode 100644 user/plugins/form/vendor/composer/ClassLoader.php create mode 100644 user/plugins/form/vendor/composer/InstalledVersions.php create mode 100644 user/plugins/form/vendor/composer/LICENSE create mode 100644 user/plugins/form/vendor/composer/autoload_classmap.php create mode 100644 user/plugins/form/vendor/composer/autoload_namespaces.php create mode 100644 user/plugins/form/vendor/composer/autoload_psr4.php create mode 100644 user/plugins/form/vendor/composer/autoload_real.php create mode 100644 user/plugins/form/vendor/composer/autoload_static.php create mode 100644 user/plugins/form/vendor/composer/installed.json create mode 100644 user/plugins/form/vendor/composer/installed.php create mode 100644 user/plugins/form/vendor/composer/platform_check.php create mode 100644 user/plugins/form/vendor/google/recaptcha/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 user/plugins/form/vendor/google/recaptcha/.gitignore create mode 100644 user/plugins/form/vendor/google/recaptcha/.travis.yml create mode 100644 user/plugins/form/vendor/google/recaptcha/ARCHITECTURE.md create mode 100644 user/plugins/form/vendor/google/recaptcha/CONTRIBUTING.md create mode 100644 user/plugins/form/vendor/google/recaptcha/LICENSE create mode 100644 user/plugins/form/vendor/google/recaptcha/README.md create mode 100644 user/plugins/form/vendor/google/recaptcha/app.yaml create mode 100644 user/plugins/form/vendor/google/recaptcha/composer.json create mode 100644 user/plugins/form/vendor/google/recaptcha/examples/appengine-https.php create mode 100644 user/plugins/form/vendor/google/recaptcha/examples/config.php.dist create mode 100644 user/plugins/form/vendor/google/recaptcha/examples/examples.css create mode 100644 user/plugins/form/vendor/google/recaptcha/examples/google0afd8760fd68f119.html create mode 100644 user/plugins/form/vendor/google/recaptcha/examples/index.php create mode 100644 user/plugins/form/vendor/google/recaptcha/examples/recaptcha-content-security-policy.php create mode 100644 user/plugins/form/vendor/google/recaptcha/examples/recaptcha-v2-checkbox-explicit.php create mode 100644 user/plugins/form/vendor/google/recaptcha/examples/recaptcha-v2-checkbox.php create mode 100644 user/plugins/form/vendor/google/recaptcha/examples/recaptcha-v2-invisible.php create mode 100644 user/plugins/form/vendor/google/recaptcha/examples/recaptcha-v3-request-scores.php create mode 100644 user/plugins/form/vendor/google/recaptcha/examples/recaptcha-v3-verify.php create mode 100644 user/plugins/form/vendor/google/recaptcha/examples/robots.txt create mode 100644 user/plugins/form/vendor/google/recaptcha/phpunit.xml.dist create mode 100644 user/plugins/form/vendor/google/recaptcha/src/ReCaptcha/ReCaptcha.php create mode 100644 user/plugins/form/vendor/google/recaptcha/src/ReCaptcha/RequestMethod.php create mode 100644 user/plugins/form/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Curl.php create mode 100644 user/plugins/form/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/CurlPost.php create mode 100644 user/plugins/form/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Post.php create mode 100644 user/plugins/form/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Socket.php create mode 100644 user/plugins/form/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/SocketPost.php create mode 100644 user/plugins/form/vendor/google/recaptcha/src/ReCaptcha/RequestParameters.php create mode 100644 user/plugins/form/vendor/google/recaptcha/src/ReCaptcha/Response.php create mode 100644 user/plugins/form/vendor/google/recaptcha/src/autoload.php create mode 100644 user/plugins/form/vendor/google/recaptcha/tests/ReCaptcha/ReCaptchaTest.php create mode 100644 user/plugins/form/vendor/google/recaptcha/tests/ReCaptcha/RequestMethod/CurlPostTest.php create mode 100644 user/plugins/form/vendor/google/recaptcha/tests/ReCaptcha/RequestMethod/PostTest.php create mode 100644 user/plugins/form/vendor/google/recaptcha/tests/ReCaptcha/RequestMethod/SocketPostTest.php create mode 100644 user/plugins/form/vendor/google/recaptcha/tests/ReCaptcha/RequestParametersTest.php create mode 100644 user/plugins/form/vendor/google/recaptcha/tests/ReCaptcha/ResponseTest.php create mode 100644 user/plugins/form/webpack.conf.js create mode 100644 user/plugins/form/yarn.lock create mode 100644 user/plugins/login/.gitignore create mode 100644 user/plugins/login/CHANGELOG.md create mode 100644 user/plugins/login/LICENSE create mode 100644 user/plugins/login/README.md create mode 100644 user/plugins/login/blueprints.yaml create mode 100644 user/plugins/login/classes/Controller.php create mode 100644 user/plugins/login/classes/Events/UserLoginEvent.php create mode 100644 user/plugins/login/classes/Invitations/Invitation.php create mode 100644 user/plugins/login/classes/Invitations/Invitations.php create mode 100644 user/plugins/login/classes/Login.php create mode 100644 user/plugins/login/classes/LoginCache.php create mode 100644 user/plugins/login/classes/RateLimiter.php create mode 100644 user/plugins/login/classes/RememberMe/RememberMe.php create mode 100644 user/plugins/login/classes/RememberMe/TokenStorage.php create mode 100644 user/plugins/login/classes/TwoFactorAuth/BaconQrProvider.php create mode 100644 user/plugins/login/classes/TwoFactorAuth/TwoFactorAuth.php create mode 100644 user/plugins/login/cli/ChangePasswordCommand.php create mode 100644 user/plugins/login/cli/ChangeUserStateCommand.php create mode 100644 user/plugins/login/cli/LookupUserCommand.php create mode 100644 user/plugins/login/cli/NewUserCommand.php create mode 100644 user/plugins/login/composer.json create mode 100644 user/plugins/login/composer.lock create mode 100644 user/plugins/login/css/login.css create mode 100644 user/plugins/login/hebe.json create mode 100644 user/plugins/login/js/2fa.js create mode 100644 user/plugins/login/languages/de.yaml create mode 100644 user/plugins/login/languages/en.yaml create mode 100644 user/plugins/login/languages/es.yaml create mode 100644 user/plugins/login/languages/fr.yaml create mode 100644 user/plugins/login/languages/hr.yaml create mode 100644 user/plugins/login/languages/hu.yaml create mode 100644 user/plugins/login/languages/lt.yaml create mode 100644 user/plugins/login/languages/no.yaml create mode 100644 user/plugins/login/languages/pt-BR.yaml create mode 100644 user/plugins/login/languages/ro.yaml create mode 100644 user/plugins/login/languages/ru.yaml create mode 100644 user/plugins/login/languages/uk.yaml create mode 100644 user/plugins/login/languages/zh.yaml create mode 100644 user/plugins/login/login.php create mode 100644 user/plugins/login/login.yaml create mode 100644 user/plugins/login/pages/forgot.md create mode 100644 user/plugins/login/pages/login.md create mode 100644 user/plugins/login/pages/profile.md create mode 100644 user/plugins/login/pages/register.md create mode 100644 user/plugins/login/pages/reset.md create mode 100644 user/plugins/login/pages/unauthorized.md create mode 100644 user/plugins/login/templates/forgot.html.twig create mode 100644 user/plugins/login/templates/forms/fields/2fa_secret/2fa_secret.html.twig create mode 100644 user/plugins/login/templates/login.html.twig create mode 100644 user/plugins/login/templates/login.json.twig create mode 100644 user/plugins/login/templates/partials/forgot-form.html.twig create mode 100644 user/plugins/login/templates/partials/login-form.html.twig create mode 100644 user/plugins/login/templates/partials/login-status.html.twig create mode 100644 user/plugins/login/templates/partials/login-twofa.html.twig create mode 100644 user/plugins/login/templates/partials/reset-form.html.twig create mode 100644 user/plugins/login/templates/profile.html.twig create mode 100644 user/plugins/login/templates/profile.json.twig create mode 100644 user/plugins/login/templates/register.html.twig create mode 100644 user/plugins/login/templates/reset.html.twig create mode 100644 user/plugins/login/templates/unauthorized.html.twig create mode 100644 user/plugins/login/vendor/autoload.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/.gitignore create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/.travis.yml create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/CHANGELOG.md create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/LICENSE create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/Module.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/README.md create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/autoload_classmap.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/autoload_function.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/autoload_register.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/composer.json create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/AbstractEnum.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/BitArray.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/BitMatrix.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/BitUtils.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/CharacterSetEci.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/EcBlock.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/EcBlocks.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/ErrorCorrectionLevel.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/FormatInformation.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/Mode.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/ReedSolomonCodec.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Common/Version.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/BlockPair.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/ByteMatrix.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/Encoder.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/MaskUtil.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/MatrixUtil.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Encoder/QrCode.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Exception/ExceptionInterface.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Exception/InvalidArgumentException.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Exception/OutOfBoundsException.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Exception/RuntimeException.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Exception/UnexpectedValueException.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Exception/WriterException.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Color/Cmyk.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Color/ColorInterface.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Color/Gray.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Color/Rgb.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/AbstractRenderer.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Decorator/DecoratorInterface.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Decorator/FinderPattern.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Eps.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Png.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/RendererInterface.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Image/Svg.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/RendererInterface.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Text/Html.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Renderer/Text/Plain.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/BaconQrCode/Writer.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Common/BitArray.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Common/BitMatrix.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Common/BitUtils.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Common/CharacterSetEci.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Common/EcBlock.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Common/EcBlocks.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Common/ErrorCorrectionLevel.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Common/FormatInformation.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Common/Mode.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Common/ReedSolomonCodec.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Common/Version.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Encoder/BlockPair.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Encoder/ByteMatrix.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Encoder/Encoder.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Encoder/MaskUtil.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Encoder/MatrixUtil.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Encoder/QrCode.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Exception/ExceptionInterface.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Exception/InvalidArgumentException.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Exception/OutOfBoundsException.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Exception/RuntimeException.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Exception/UnexpectedValueException.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Exception/WriterException.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Color/Alpha.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Color/Cmyk.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Color/ColorInterface.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Color/Gray.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Color/Rgb.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Eye/CompositeEye.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Eye/EyeInterface.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Eye/ModuleEye.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Eye/SimpleCircleEye.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Eye/SquareEye.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Image/EpsImageBackEnd.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Image/ImageBackEndInterface.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Image/ImagickImageBackEnd.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Image/SvgImageBackEnd.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Image/TransformationMatrix.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/ImageRenderer.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Module/DotsModule.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Module/EdgeIterator/Edge.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Module/EdgeIterator/EdgeIterator.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Module/ModuleInterface.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Module/RoundnessModule.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Module/SquareModule.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Path/Close.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Path/Curve.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Path/EllipticArc.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Path/Line.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Path/Move.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Path/OperationInterface.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/Path/Path.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/PlainTextRenderer.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/RendererInterface.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/RendererStyle/EyeFill.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/RendererStyle/Fill.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/RendererStyle/Gradient.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/RendererStyle/GradientType.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Renderer/RendererStyle/RendererStyle.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/src/Writer.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/BitArrayTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/BitMatrixTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/BitUtilsTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/ErrorCorrectionLevelTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/FormatInformationTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/ModeTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/ReedSolomonCodecTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Common/VersionTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Encoder/EncoderTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Encoder/MaskUtilTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Encoder/MatrixUtilTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Renderer/Text/HtmlTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/BaconQrCode/Renderer/Text/TextTest.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/bootstrap.php create mode 100644 user/plugins/login/vendor/bacon/bacon-qr-code/tests/phpunit.xml create mode 100644 user/plugins/login/vendor/birke/rememberme/.gitignore create mode 100644 user/plugins/login/vendor/birke/rememberme/LICENSE create mode 100644 user/plugins/login/vendor/birke/rememberme/README.md create mode 100644 user/plugins/login/vendor/birke/rememberme/composer.json create mode 100644 user/plugins/login/vendor/birke/rememberme/example/action.php create mode 100644 user/plugins/login/vendor/birke/rememberme/example/css/style.css create mode 100644 user/plugins/login/vendor/birke/rememberme/example/index.php create mode 100644 user/plugins/login/vendor/birke/rememberme/example/templates/cookie_was_stolen.php create mode 100644 user/plugins/login/vendor/birke/rememberme/example/templates/login.php create mode 100644 user/plugins/login/vendor/birke/rememberme/example/templates/user_is_logged_in.php create mode 100644 user/plugins/login/vendor/birke/rememberme/phpunit.xml create mode 100644 user/plugins/login/vendor/birke/rememberme/src/Rememberme/Authenticator.php create mode 100644 user/plugins/login/vendor/birke/rememberme/src/Rememberme/Cookie.php create mode 100644 user/plugins/login/vendor/birke/rememberme/src/Rememberme/Storage/DB.php create mode 100644 user/plugins/login/vendor/birke/rememberme/src/Rememberme/Storage/File.php create mode 100644 user/plugins/login/vendor/birke/rememberme/src/Rememberme/Storage/PDO.php create mode 100644 user/plugins/login/vendor/birke/rememberme/src/Rememberme/Storage/StorageInterface.php create mode 100644 user/plugins/login/vendor/birke/rememberme/test/CookieTest.php create mode 100644 user/plugins/login/vendor/birke/rememberme/test/RemembermeTest.php create mode 100644 user/plugins/login/vendor/birke/rememberme/test/Storage/PDO.php create mode 100644 user/plugins/login/vendor/birke/rememberme/test/Storage/tokens.xml create mode 100644 user/plugins/login/vendor/birke/rememberme/test/bootstrap.php create mode 100644 user/plugins/login/vendor/composer/ClassLoader.php create mode 100644 user/plugins/login/vendor/composer/InstalledVersions.php create mode 100644 user/plugins/login/vendor/composer/LICENSE create mode 100644 user/plugins/login/vendor/composer/autoload_classmap.php create mode 100644 user/plugins/login/vendor/composer/autoload_files.php create mode 100644 user/plugins/login/vendor/composer/autoload_namespaces.php create mode 100644 user/plugins/login/vendor/composer/autoload_psr4.php create mode 100644 user/plugins/login/vendor/composer/autoload_real.php create mode 100644 user/plugins/login/vendor/composer/autoload_static.php create mode 100644 user/plugins/login/vendor/composer/installed.json create mode 100644 user/plugins/login/vendor/composer/installed.php create mode 100644 user/plugins/login/vendor/composer/platform_check.php create mode 100644 user/plugins/login/vendor/dasprid/enum/.coveralls.yml create mode 100644 user/plugins/login/vendor/dasprid/enum/.gitignore create mode 100644 user/plugins/login/vendor/dasprid/enum/.travis.yml create mode 100644 user/plugins/login/vendor/dasprid/enum/README.md create mode 100644 user/plugins/login/vendor/dasprid/enum/composer.json create mode 100644 user/plugins/login/vendor/dasprid/enum/phpcs.xml create mode 100644 user/plugins/login/vendor/dasprid/enum/phpunit.xml.dist create mode 100644 user/plugins/login/vendor/dasprid/enum/src/AbstractEnum.php create mode 100644 user/plugins/login/vendor/dasprid/enum/src/EnumMap.php create mode 100644 user/plugins/login/vendor/dasprid/enum/src/Exception/CloneNotSupportedException.php create mode 100644 user/plugins/login/vendor/dasprid/enum/src/Exception/ExceptionInterface.php create mode 100644 user/plugins/login/vendor/dasprid/enum/src/Exception/ExpectationException.php create mode 100644 user/plugins/login/vendor/dasprid/enum/src/Exception/IllegalArgumentException.php create mode 100644 user/plugins/login/vendor/dasprid/enum/src/Exception/MismatchException.php create mode 100644 user/plugins/login/vendor/dasprid/enum/src/Exception/SerializeNotSupportedException.php create mode 100644 user/plugins/login/vendor/dasprid/enum/src/Exception/UnserializeNotSupportedException.php create mode 100644 user/plugins/login/vendor/dasprid/enum/src/NullValue.php create mode 100644 user/plugins/login/vendor/dasprid/enum/test/AbstractEnumTest.php create mode 100644 user/plugins/login/vendor/dasprid/enum/test/EnumMapTest.php create mode 100644 user/plugins/login/vendor/dasprid/enum/test/NullValueTest.php create mode 100644 user/plugins/login/vendor/dasprid/enum/test/Planet.php create mode 100644 user/plugins/login/vendor/dasprid/enum/test/WeekDay.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/CHANGELOG.md create mode 100644 user/plugins/login/vendor/paragonie/random_compat/LICENSE create mode 100644 user/plugins/login/vendor/paragonie/random_compat/RATIONALE.md create mode 100644 user/plugins/login/vendor/paragonie/random_compat/README.md create mode 100644 user/plugins/login/vendor/paragonie/random_compat/SECURITY.md create mode 100644 user/plugins/login/vendor/paragonie/random_compat/build-phar.sh create mode 100644 user/plugins/login/vendor/paragonie/random_compat/composer.json create mode 100644 user/plugins/login/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey create mode 100644 user/plugins/login/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc create mode 100644 user/plugins/login/vendor/paragonie/random_compat/lib/byte_safe_strings.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/lib/cast_to_int.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/lib/error_polyfill.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/lib/random.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/lib/random_bytes_com_dotnet.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/lib/random_bytes_dev_urandom.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/lib/random_bytes_libsodium.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/lib/random_bytes_libsodium_legacy.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/lib/random_bytes_mcrypt.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/lib/random_bytes_openssl.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/lib/random_int.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/other/build_phar.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/other/ide_stubs/COM.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/other/ide_stubs/README.md create mode 100644 user/plugins/login/vendor/paragonie/random_compat/other/ide_stubs/com_exception.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/other/ide_stubs/libsodium.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/psalm-autoload.php create mode 100644 user/plugins/login/vendor/paragonie/random_compat/psalm.xml create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/.github/FUNDING.yml create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/.github/workflows/test.yml create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/.gitignore create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/LICENSE create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/README.md create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/TwoFactorAuth.phpproj create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/TwoFactorAuth.sln create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/composer.json create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/demo/demo.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/demo/loader.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Qr/BaconQrCodeProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Qr/BaseHTTPQRCodeProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Qr/EndroidQrCodeProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Qr/EndroidQrCodeWithLogoProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Qr/IQRCodeProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Qr/ImageChartsQRCodeProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Qr/QRException.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Qr/QRServerProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Qr/QRicketProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Rng/CSRNGProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Rng/HashRNGProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Rng/IRNGProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Rng/MCryptRNGProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Rng/OpenSSLRNGProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Rng/RNGException.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Time/HttpTimeProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Time/ITimeProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Time/LocalMachineTimeProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Time/NTPTimeProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/Providers/Time/TimeException.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/TwoFactorAuth.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/lib/TwoFactorAuthException.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/logo.png create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/multifactorauthforeveryone.png create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/phpunit.xml create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/MightNotMakeAssertions.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/Providers/Qr/IQRCodeProviderTest.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/Providers/Qr/TestQrProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/Providers/Rng/CSRNGProviderTest.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/Providers/Rng/HashRNGProviderTest.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/Providers/Rng/IRNGProviderTest.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/Providers/Rng/MCryptRNGProviderTest.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/Providers/Rng/NeedsRngLengths.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/Providers/Rng/OpenSSLRNGProviderTest.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/Providers/Rng/TestRNGProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/Providers/Time/ITimeProviderTest.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/Providers/Time/TestTimeProvider.php create mode 100644 user/plugins/login/vendor/robthree/twofactorauth/tests/TwoFactorAuthTest.php create mode 100644 user/plugins/sitemap/.gitignore create mode 100644 user/plugins/sitemap/CHANGELOG.md create mode 100644 user/plugins/sitemap/LICENSE create mode 100644 user/plugins/sitemap/README.md create mode 100644 user/plugins/sitemap/blueprints.yaml create mode 100644 user/plugins/sitemap/blueprints/sitemap.yaml create mode 100644 user/plugins/sitemap/classes/SitemapEntry.php create mode 100644 user/plugins/sitemap/composer.json create mode 100644 user/plugins/sitemap/composer.lock create mode 100644 user/plugins/sitemap/hebe.json create mode 100644 user/plugins/sitemap/languages.yaml create mode 100644 user/plugins/sitemap/pages/sitemap.md create mode 100644 user/plugins/sitemap/sitemap.php create mode 100644 user/plugins/sitemap/sitemap.xsl create mode 100644 user/plugins/sitemap/sitemap.yaml create mode 100644 user/plugins/sitemap/templates/sitemap.html.twig create mode 100644 user/plugins/sitemap/templates/sitemap.json.twig create mode 100644 user/plugins/sitemap/templates/sitemap.xml.twig create mode 100644 user/plugins/sitemap/vendor/autoload.php create mode 100644 user/plugins/sitemap/vendor/composer/ClassLoader.php create mode 100644 user/plugins/sitemap/vendor/composer/InstalledVersions.php create mode 100644 user/plugins/sitemap/vendor/composer/LICENSE create mode 100644 user/plugins/sitemap/vendor/composer/autoload_classmap.php create mode 100644 user/plugins/sitemap/vendor/composer/autoload_namespaces.php create mode 100644 user/plugins/sitemap/vendor/composer/autoload_psr4.php create mode 100644 user/plugins/sitemap/vendor/composer/autoload_real.php create mode 100644 user/plugins/sitemap/vendor/composer/autoload_static.php create mode 100644 user/plugins/sitemap/vendor/composer/installed.json create mode 100644 user/plugins/sitemap/vendor/composer/installed.php create mode 100644 user/plugins/sitemap/vendor/composer/platform_check.php create mode 100644 user/plugins/taxonomylist/.gitignore create mode 100644 user/plugins/taxonomylist/CHANGELOG.md create mode 100644 user/plugins/taxonomylist/LICENSE create mode 100644 user/plugins/taxonomylist/README.md create mode 100644 user/plugins/taxonomylist/assets/readme_1.png create mode 100644 user/plugins/taxonomylist/blueprints.yaml create mode 100644 user/plugins/taxonomylist/classes/taxonomylist.php create mode 100644 user/plugins/taxonomylist/composer.json create mode 100644 user/plugins/taxonomylist/composer.lock create mode 100644 user/plugins/taxonomylist/hebe.json create mode 100644 user/plugins/taxonomylist/taxonomylist.php create mode 100644 user/plugins/taxonomylist/taxonomylist.yaml create mode 100644 user/plugins/taxonomylist/templates/partials/taxonomylist.html.twig create mode 100644 user/plugins/taxonomylist/vendor/autoload.php create mode 100644 user/plugins/taxonomylist/vendor/composer/ClassLoader.php create mode 100644 user/plugins/taxonomylist/vendor/composer/LICENSE create mode 100644 user/plugins/taxonomylist/vendor/composer/autoload_classmap.php create mode 100644 user/plugins/taxonomylist/vendor/composer/autoload_namespaces.php create mode 100644 user/plugins/taxonomylist/vendor/composer/autoload_psr4.php create mode 100644 user/plugins/taxonomylist/vendor/composer/autoload_real.php create mode 100644 user/plugins/taxonomylist/vendor/composer/autoload_static.php create mode 100644 user/plugins/taxonomylist/vendor/composer/installed.json create mode 100644 user/themes/.gitkeep create mode 100644 user/themes/figureslibres-v2/CHANGELOG.md create mode 100644 user/themes/figureslibres-v2/LICENSE create mode 100644 user/themes/figureslibres-v2/README.md create mode 100644 user/themes/figureslibres-v2/blueprints.yaml create mode 100644 user/themes/figureslibres-v2/css/custom.css create mode 100644 user/themes/figureslibres-v2/css/owl.carousel.min.css create mode 100644 user/themes/figureslibres-v2/css/theme.css create mode 100644 user/themes/figureslibres-v2/css/theme.css.map create mode 100644 user/themes/figureslibres-v2/figureslibres-v2.php create mode 100644 user/themes/figureslibres-v2/figureslibres-v2.yaml create mode 100644 user/themes/figureslibres-v2/fonts/Avara/Avara-bold-italic/Avara-bold-italic.ttf create mode 100644 user/themes/figureslibres-v2/fonts/Avara/Avara-bold-italic/Avara-bold-italic.woff create mode 100644 user/themes/figureslibres-v2/fonts/Avara/Avara-bold-italic/Avara-bold-italic.woff2 create mode 100644 user/themes/figureslibres-v2/fonts/Lato/Lato-bold-italic/Lato-bold-italic.ttf create mode 100644 user/themes/figureslibres-v2/fonts/Lato/Lato-bold-italic/Lato-bold-italic.woff create mode 100644 user/themes/figureslibres-v2/fonts/Lato/Lato-bold-italic/Lato-bold-italic.woff2 create mode 100644 user/themes/figureslibres-v2/fonts/Lato/Lato-bold/Lato-bold.ttf create mode 100644 user/themes/figureslibres-v2/fonts/Lato/Lato-bold/Lato-bold.woff create mode 100644 user/themes/figureslibres-v2/fonts/Lato/Lato-bold/Lato-bold.woff2 create mode 100644 user/themes/figureslibres-v2/fonts/Lato/Lato-italic/Lato-italic.ttf create mode 100644 user/themes/figureslibres-v2/fonts/Lato/Lato-italic/Lato-italic.woff create mode 100644 user/themes/figureslibres-v2/fonts/Lato/Lato-italic/Lato-italic.woff2 create mode 100644 user/themes/figureslibres-v2/fonts/Lato/Lato-regular/Lato-regular.ttf create mode 100644 user/themes/figureslibres-v2/fonts/Lato/Lato-regular/Lato-regular.woff create mode 100644 user/themes/figureslibres-v2/fonts/Lato/Lato-regular/Lato-regular.woff2 create mode 100644 user/themes/figureslibres-v2/fonts/Lato/stylesheet.css create mode 100644 user/themes/figureslibres-v2/fonts/ManifontGrotesk/ManifontGrotesk-bold-italic/ManifontGrotesk-bold-italic.ttf create mode 100644 user/themes/figureslibres-v2/fonts/ManifontGrotesk/ManifontGrotesk-bold-italic/ManifontGrotesk-bold-italic.woff create mode 100644 user/themes/figureslibres-v2/fonts/ManifontGrotesk/ManifontGrotesk-bold-italic/ManifontGrotesk-bold-italic.woff2 create mode 100644 user/themes/figureslibres-v2/fonts/ManifontGrotesk/ManifontGrotesk-italic/ManifontGrotesk-italic.ttf create mode 100644 user/themes/figureslibres-v2/fonts/ManifontGrotesk/ManifontGrotesk-italic/ManifontGrotesk-italic.woff create mode 100644 user/themes/figureslibres-v2/fonts/ManifontGrotesk/ManifontGrotesk-italic/ManifontGrotesk-italic.woff2 create mode 100644 user/themes/figureslibres-v2/fonts/Moche/Moche-bold/Moche-bold.ttf create mode 100644 user/themes/figureslibres-v2/fonts/Moche/Moche-bold/Moche-bold.woff create mode 100644 user/themes/figureslibres-v2/fonts/Moche/Moche-bold/Moche-bold.woff2 create mode 100644 user/themes/figureslibres-v2/fonts/PlayfairDisplay/PlayfairDisplay-italic/PlayfairDisplay-italic.ttf create mode 100644 user/themes/figureslibres-v2/fonts/PlayfairDisplay/PlayfairDisplay-italic/PlayfairDisplay-italic.woff create mode 100644 user/themes/figureslibres-v2/fonts/PlayfairDisplay/PlayfairDisplay-italic/PlayfairDisplay-italic.woff2 create mode 100644 user/themes/figureslibres-v2/fonts/Redaction50/Redaction50-bold/Redaction50-bold.ttf create mode 100644 user/themes/figureslibres-v2/fonts/Redaction50/Redaction50-bold/Redaction50-bold.woff create mode 100644 user/themes/figureslibres-v2/fonts/Redaction50/Redaction50-bold/Redaction50-bold.woff2 create mode 100644 user/themes/figureslibres-v2/fonts/Rumeur/Rumeur-bold/Rumeur-bold.ttf create mode 100644 user/themes/figureslibres-v2/fonts/Rumeur/Rumeur-bold/Rumeur-bold.woff create mode 100644 user/themes/figureslibres-v2/fonts/Rumeur/Rumeur-bold/Rumeur-bold.woff2 create mode 100644 user/themes/figureslibres-v2/fonts/Syne/Syne-bold/Syne-bold.ttf create mode 100644 user/themes/figureslibres-v2/fonts/Syne/Syne-bold/Syne-bold.woff create mode 100644 user/themes/figureslibres-v2/fonts/Syne/Syne-bold/Syne-bold.woff2 create mode 100644 user/themes/figureslibres-v2/fonts/Syne/Syne-regular/Syne-regular.ttf create mode 100644 user/themes/figureslibres-v2/fonts/Syne/Syne-regular/Syne-regular.woff create mode 100644 user/themes/figureslibres-v2/fonts/Syne/Syne-regular/Syne-regular.woff2 create mode 100644 user/themes/figureslibres-v2/images/favicon.png create mode 100644 user/themes/figureslibres-v2/images/loader/circle.mp4 create mode 100644 user/themes/figureslibres-v2/images/loader/rolling.svg create mode 100644 user/themes/figureslibres-v2/images/pictos/arrow-diagonal.svg create mode 100644 user/themes/figureslibres-v2/images/pictos/arrow-down.svg create mode 100644 user/themes/figureslibres-v2/images/pictos/arrow-left.svg create mode 100644 user/themes/figureslibres-v2/images/pictos/arrow-right.svg create mode 100644 user/themes/figureslibres-v2/images/pictos/burger.svg create mode 100644 user/themes/figureslibres-v2/images/pictos/chevron-down.svg create mode 100644 user/themes/figureslibres-v2/images/pictos/cross.svg create mode 100644 user/themes/figureslibres-v2/js/libraries/blotter/blotter.min.js create mode 100644 user/themes/figureslibres-v2/js/libraries/blotter/liquidDistortMaterial.js create mode 100644 user/themes/figureslibres-v2/js/libraries/gsap/gsap.min.js create mode 100644 user/themes/figureslibres-v2/js/libraries/lazy/jquery.lazy.min.js create mode 100644 user/themes/figureslibres-v2/js/libraries/owl.carousel.min.js create mode 100644 user/themes/figureslibres-v2/js/script.js create mode 100644 user/themes/figureslibres-v2/screenshot.jpg create mode 100644 user/themes/figureslibres-v2/scss/configuration/_fonts.scss create mode 100644 user/themes/figureslibres-v2/scss/configuration/_reset.css create mode 100644 user/themes/figureslibres-v2/scss/configuration/_variable.scss create mode 100644 user/themes/figureslibres-v2/scss/style.scss create mode 100644 user/themes/figureslibres-v2/scss/theme.scss create mode 100644 user/themes/figureslibres-v2/templates/default.html.twig create mode 100644 user/themes/figureslibres-v2/templates/error.html.twig create mode 100644 user/themes/figureslibres-v2/templates/index.html.twig create mode 100644 user/themes/figureslibres-v2/templates/partials/base.html.twig create mode 100644 user/themes/figureslibres-v2/templates/partials/carrousel.html.twig create mode 100644 user/themes/figureslibres-v2/templates/partials/extrait-projets.html.twig create mode 100644 user/themes/figureslibres-v2/templates/partials/navigation.html.twig create mode 100644 user/themes/figureslibres-v2/templates/partials/projet_card.html.twig create mode 100644 user/themes/figureslibres-v2/templates/partials/relatedpages.html.twig create mode 100644 user/themes/figureslibres-v2/templates/projets-index.html.twig create mode 100644 user/themes/figureslibres-v2/templates/reader.html.twig create mode 100644 user/themes/figureslibres-v2/templates/text-content.html.twig create mode 100644 user/themes/figureslibres-v2/thumbnail.jpg create mode 100644 webserver-configs/Caddyfile create mode 100644 webserver-configs/Caddyfile-0.8.x create mode 100644 webserver-configs/htaccess.txt create mode 100644 webserver-configs/lighttpd.conf create mode 100644 webserver-configs/nginx.conf create mode 100644 webserver-configs/web.config diff --git a/.dependencies b/.dependencies new file mode 100644 index 0000000..86f4a79 --- /dev/null +++ b/.dependencies @@ -0,0 +1,34 @@ +git: + problems: + url: https://github.com/getgrav/grav-plugin-problems + path: user/plugins/problems + branch: master + error: + url: https://github.com/getgrav/grav-plugin-error + path: user/plugins/error + branch: master + markdown-notices: + url: https://github.com/getgrav/grav-plugin-markdown-notices + path: user/plugins/markdown-notices + branch: master + quark: + url: https://github.com/getgrav/grav-theme-quark + path: user/themes/quark + branch: master +links: + problems: + src: grav-plugin-problems + path: user/plugins/problems + scm: github + error: + src: grav-plugin-error + path: user/plugins/error + scm: github + markdown-notices: + src: grav-plugin-markdown-notices + path: user/plugins/markdown-notices + scm: github + quark: + src: grav-theme-quark + path: user/themes/quark + scm: github diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3b95e6d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +# 2 space indentation +[*.{yaml,.yml}] +indent_size = 2 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e84f52b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: grav +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: # Replace with a single custom sponsorship URL diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59e5657 --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +#Test commit prod + +# Composer +.composer +vendor/* +!*/vendor/* + +# Sass +.sass-cache + +# Grav Specific +backup/* +!backup/.* +cache/* +!cache/.* +tmp/* +!tmp/.* +assets/* +!assets/.* +logs/* +!logs/.* +images/* +!images/.* +user/data/* +!user/data/.* +user/themes/quark/ +!user/themes/quark/.* +user/themes/figureslibres/* +user/pages/* +!user/pages/.* +user/localhost/config/security.yaml +user/config/security.yaml +user/accounts/* +user/accounts/.* +user/plugins/problems +user/plugins/error +user/plugins/markdown-notices + +# OS Generated +.DS_Store* +ehthumbs.db +Icon? +Thumbs.db +*.swp + + +# phpstorm +.idea/* + +# testing stuff +tests/_output/* +tests/_support/_generated/* +tests/cache/* +tests/error.log +system/templates/testing/* diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6bc375c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "plugin-error"] + path = user/plugins/error + url = https://github.com/getgrav/grav-plugin-error +[submodule "plugin-problems"] + path = user/plugins/problems + url = https://github.com/getgrav/grav-plugin-problems +[submodule "plugin-markdown"] + path = user/plugins/markdown-notices + url = https://github.com/getgrav/grav-plugin-markdown-notices diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..d850bdc --- /dev/null +++ b/.htaccess @@ -0,0 +1,80 @@ + + +RewriteEngine On + +## Begin RewriteBase +# If you are getting 500 or 404 errors on subpages, you may have to uncomment the RewriteBase entry +# You should change the '/' to your appropriate subfolder. For example if you have +# your Grav install at the root of your site '/' should work, else it might be something +# along the lines of: RewriteBase / +## + +# RewriteBase / + +## End - RewriteBase + +## Begin - X-Forwarded-Proto +# In some hosted or load balanced environments, SSL negotiation happens upstream. +# In order for Grav to recognize the connection as secure, you need to uncomment +# the following lines. +# +# RewriteCond %{HTTP:X-Forwarded-Proto} https +# RewriteRule .* - [E=HTTPS:on] +# +## End - X-Forwarded-Proto + +## Begin - Exploits +# If you experience problems on your site block out the operations listed below +# This attempts to block the most common type of exploit `attempts` to Grav +# +# Block out any script trying to base64_encode data within the URL. +RewriteCond %{QUERY_STRING} base64_encode[^(]*\([^)]*\) [OR] +# Block out any script that includes a \n"; + } +} diff --git a/system/src/Grav/Common/Assets/Js.php b/system/src/Grav/Common/Assets/Js.php new file mode 100644 index 0000000..fc2a472 --- /dev/null +++ b/system/src/Grav/Common/Assets/Js.php @@ -0,0 +1,48 @@ + 'js', + ]; + + $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements); + + parent::__construct($merged_attributes, $key); + } + + /** + * @return string + */ + public function render() + { + if (isset($this->attributes['loading']) && $this->attributes['loading'] === 'inline') { + $buffer = $this->gatherLinks([$this], self::JS_ASSET); + return 'renderAttributes() . ">\n" . trim($buffer) . "\n\n"; + } + + return '\n"; + } +} diff --git a/system/src/Grav/Common/Assets/Pipeline.php b/system/src/Grav/Common/Assets/Pipeline.php new file mode 100644 index 0000000..948104a --- /dev/null +++ b/system/src/Grav/Common/Assets/Pipeline.php @@ -0,0 +1,285 @@ +base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/'; + $this->assets_dir = $locator->findResource('asset://'); + if (!$this->assets_dir) { + // Attempt to create assets folder if it doesn't exist yet. + $this->assets_dir = $locator->findResource('asset://', true, true); + Folder::mkdir($this->assets_dir); + $locator->clearCache(); + } + + $this->assets_url = $locator->findResource('asset://', false); + } + + /** + * Minify and concatenate CSS + * + * @param array $assets + * @param string $group + * @param array $attributes + * @return bool|string URL or generated content if available, else false + */ + public function renderCss($assets, $group, $attributes = []) + { + // temporary list of assets to pipeline + $inline_group = false; + + if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') { + $inline_group = true; + unset($attributes['loading']); + } + + // Store Attributes + $this->attributes = array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes); + + // Compute uid based on assets and timestamp + $json_assets = json_encode($assets); + $uid = md5($json_assets . $this->css_minify . $this->css_rewrite . $group); + $file = $uid . '.css'; + $relative_path = "{$this->base_url}{$this->assets_url}/{$file}"; + + $filepath = "{$this->assets_dir}/{$file}"; + if (file_exists($filepath)) { + $buffer = file_get_contents($filepath) . "\n"; + } else { + //if nothing found get out of here! + if (empty($assets)) { + return false; + } + + // Concatenate files + $buffer = $this->gatherLinks($assets, self::CSS_ASSET); + + // Minify if required + if ($this->shouldMinify('css')) { + $minifier = new CSS(); + $minifier->add($buffer); + $buffer = $minifier->minify(); + } + + // Write file + if (trim($buffer) !== '') { + file_put_contents($filepath, $buffer); + } + } + + if ($inline_group) { + $output = "\n"; + } else { + $this->asset = $relative_path; + $output = 'renderAttributes() . BaseAsset::integrityHash($this->asset) . ">\n"; + } + + return $output; + } + + /** + * Minify and concatenate JS files. + * + * @param array $assets + * @param string $group + * @param array $attributes + * @return bool|string URL or generated content if available, else false + */ + public function renderJs($assets, $group, $attributes = []) + { + // temporary list of assets to pipeline + $inline_group = false; + + if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') { + $inline_group = true; + unset($attributes['loading']); + } + + // Store Attributes + $this->attributes = $attributes; + + // Compute uid based on assets and timestamp + $json_assets = json_encode($assets); + $uid = md5($json_assets . $this->js_minify . $group); + $file = $uid . '.js'; + $relative_path = "{$this->base_url}{$this->assets_url}/{$file}"; + + $filepath = "{$this->assets_dir}/{$file}"; + if (file_exists($filepath)) { + $buffer = file_get_contents($filepath) . "\n"; + } else { + //if nothing found get out of here! + if (empty($assets)) { + return false; + } + + // Concatenate files + $buffer = $this->gatherLinks($assets, self::JS_ASSET); + + // Minify if required + if ($this->shouldMinify('js')) { + $minifier = new JS(); + $minifier->add($buffer); + $buffer = $minifier->minify(); + } + + // Write file + if (trim($buffer) !== '') { + file_put_contents($filepath, $buffer); + } + } + + if ($inline_group) { + $output = 'renderAttributes(). ">\n" . $buffer . "\n\n"; + } else { + $this->asset = $relative_path; + $output = '\n"; + } + + return $output; + } + + + /** + * Finds relative CSS urls() and rewrites the URL with an absolute one + * + * @param string $file the css source file + * @param string $dir , $local relative path to the css file + * @param bool $local is this a local or remote asset + * @return string + */ + protected function cssRewrite($file, $dir, $local) + { + // Strip any sourcemap comments + $file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file); + + // Find any css url() elements, grab the URLs and calculate an absolute path + // Then replace the old url with the new one + $file = (string)preg_replace_callback(self::CSS_URL_REGEX, function ($matches) use ($dir, $local) { + + $old_url = $matches[2]; + + // Ensure link is not rooted to web server, a data URL, or to a remote host + if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url) || Utils::startsWith($old_url, 'data:') || $this->isRemoteLink($old_url)) { + return $matches[0]; + } + + // clean leading / + $old_url = Utils::normalizePath($dir . '/' . $old_url); + if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url)) { + $old_url = ltrim($old_url, '/'); + } + + $new_url = ($local ? $this->base_url : '') . $old_url; + + return str_replace($matches[2], $new_url, $matches[0]); + }, $file); + + return $file; + } + + /** + * @param string $type + * @return bool + */ + private function shouldMinify($type = 'css') + { + $check = $type . '_minify'; + $win_check = $type . '_minify_windows'; + + $minify = (bool) $this->$check; + + // If this is a Windows server, and minify_windows is false (default value) skip the + // minification process because it will cause Apache to die/crash due to insufficient + // ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689 + if (stripos(php_uname('s'), 'WIN') === 0 && !$this->{$win_check}) { + $minify = false; + } + + return $minify; + } +} diff --git a/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php b/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php new file mode 100644 index 0000000..3f5a690 --- /dev/null +++ b/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php @@ -0,0 +1,210 @@ +rootUrl(true); + + // Sanity check for local URLs with absolute URL's enabled + if (Utils::startsWith($link, $base)) { + return false; + } + + return (0 === strpos($link, 'http://') || 0 === strpos($link, 'https://') || 0 === strpos($link, '//')); + } + + /** + * Download and concatenate the content of several links. + * + * @param array $assets + * @param bool $css + * @return string + */ + protected function gatherLinks(array $assets, $css = true) + { + $buffer = ''; + foreach ($assets as $id => $asset) { + $local = true; + + $link = $asset->getAsset(); + $relative_path = $link; + + if (static::isRemoteLink($link)) { + $local = false; + if (0 === strpos($link, '//')) { + $link = 'http:' . $link; + } + $relative_dir = dirname($relative_path); + } else { + // Fix to remove relative dir if grav is in one + if (($this->base_url !== '/') && Utils::startsWith($relative_path, $this->base_url)) { + $base_url = '#' . preg_quote($this->base_url, '#') . '#'; + $relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/'); + } + + $relative_dir = dirname($relative_path); + $link = GRAV_ROOT . '/' . $relative_path; + } + + // TODO: looks like this is not being used. + $file = $this->fetch_command instanceof Closure ? @$this->fetch_command->__invoke($link) : @file_get_contents($link); + + // No file found, skip it... + if ($file === false) { + continue; + } + + // Double check last character being + if (!$css) { + $file = rtrim($file, ' ;') . ';'; + } + + // If this is CSS + the file is local + rewrite enabled + if ($css && $this->css_rewrite) { + $file = $this->cssRewrite($file, $relative_dir, $local); + } + + $file = rtrim($file) . PHP_EOL; + $buffer .= $file; + } + + // Pull out @imports and move to top + if ($css) { + $buffer = $this->moveImports($buffer); + } + + return $buffer; + } + + /** + * Moves @import statements to the top of the file per the CSS specification + * + * @param string $file the file containing the combined CSS files + * @return string the modified file with any @imports at the top of the file + */ + protected function moveImports($file) + { + $regex = '{@import.*?["\']([^"\']+)["\'].*?;}'; + + $imports = []; + + $file = (string)preg_replace_callback($regex, static function ($matches) use (&$imports) { + $imports[] = $matches[0]; + + return ''; + }, $file); + + return implode("\n", $imports) . "\n\n" . $file; + } + + /** + * + * Build an HTML attribute string from an array. + * + * @return string + */ + protected function renderAttributes() + { + $html = ''; + $no_key = ['loading']; + + foreach ($this->attributes as $key => $value) { + if ($value === null) { + continue; + } + + if (is_numeric($key)) { + $key = $value; + } + if (is_array($value)) { + $value = implode(' ', $value); + } + + if (in_array($key, $no_key, true)) { + $element = htmlentities($value, ENT_QUOTES, 'UTF-8', false); + } else { + $element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"'; + } + + $html .= ' ' . $element; + } + + return $html; + } + + /** + * Render Querystring + * + * @param string|null $asset + * @return string + */ + protected function renderQueryString($asset = null) + { + $querystring = ''; + + $asset = $asset ?? $this->asset; + + if (!empty($this->query)) { + if (Utils::contains($asset, '?')) { + $querystring .= '&' . $this->query; + } else { + $querystring .= '?' . $this->query; + } + } + + if ($this->timestamp) { + if ($querystring || Utils::contains($asset, '?')) { + $querystring .= '&' . $this->timestamp; + } else { + $querystring .= '?' . $this->timestamp; + } + } + + return $querystring; + } +} diff --git a/system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php b/system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php new file mode 100644 index 0000000..36b2152 --- /dev/null +++ b/system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php @@ -0,0 +1,137 @@ + null, 'pipeline' => true, 'loading' => null, 'group' => null]; + $arguments = $this->createArgumentsFromLegacy($args, $defaults); + break; + + case (Assets::INLINE_JS_TYPE): + $defaults = ['priority' => null, 'group' => null, 'attributes' => null]; + $arguments = $this->createArgumentsFromLegacy($args, $defaults); + + // special case to handle old attributes being passed in + if (isset($arguments['attributes'])) { + $old_attributes = $arguments['attributes']; + if (is_array($old_attributes)) { + $arguments = array_merge($arguments, $old_attributes); + } else { + $arguments['type'] = $old_attributes; + } + } + unset($arguments['attributes']); + + break; + + case (Assets::INLINE_CSS_TYPE): + $defaults = ['priority' => null, 'group' => null]; + $arguments = $this->createArgumentsFromLegacy($args, $defaults); + break; + + default: + case (Assets::CSS_TYPE): + $defaults = ['priority' => null, 'pipeline' => true, 'group' => null, 'loading' => null]; + $arguments = $this->createArgumentsFromLegacy($args, $defaults); + } + + return $arguments; + } + + /** + * @param array $args + * @param array $defaults + * @return array + */ + protected function createArgumentsFromLegacy(array $args, array $defaults) + { + // Remove arguments with old default values. + $arguments = []; + foreach ($args as $arg) { + $default = current($defaults); + if ($arg !== $default) { + $arguments[key($defaults)] = $arg; + } + next($defaults); + } + + return $arguments; + } + + /** + * Convenience wrapper for async loading of JavaScript + * + * @param string|array $asset + * @param int $priority + * @param bool $pipeline + * @param string $group name of the group + * @return Assets + * @deprecated Please use dynamic method with ['loading' => 'async']. + */ + public function addAsyncJs($asset, $priority = 10, $pipeline = true, $group = 'head') + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'async\']', E_USER_DEPRECATED); + + return $this->addJs($asset, $priority, $pipeline, 'async', $group); + } + + /** + * Convenience wrapper for deferred loading of JavaScript + * + * @param string|array $asset + * @param int $priority + * @param bool $pipeline + * @param string $group name of the group + * @return Assets + * @deprecated Please use dynamic method with ['loading' => 'defer']. + */ + public function addDeferJs($asset, $priority = 10, $pipeline = true, $group = 'head') + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'defer\']', E_USER_DEPRECATED); + + return $this->addJs($asset, $priority, $pipeline, 'defer', $group); + } +} diff --git a/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php b/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php new file mode 100644 index 0000000..15dc00e --- /dev/null +++ b/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php @@ -0,0 +1,341 @@ +collections[$asset]) || isset($this->assets_css[$asset]) || isset($this->assets_js[$asset]); + } + + /** + * Return the array of all the registered collections + * + * @return array + */ + public function getCollections() + { + return $this->collections; + } + + /** + * Set the array of collections explicitly + * + * @param array $collections + * @return $this + */ + public function setCollection($collections) + { + $this->collections = $collections; + + return $this; + } + + /** + * Return the array of all the registered CSS assets + * If a $key is provided, it will try to return only that asset + * else it will return null + * + * @param string|null $key the asset key + * @return array + */ + public function getCss($key = null) + { + if (null !== $key) { + $asset_key = md5($key); + + return $this->assets_css[$asset_key] ?? null; + } + + return $this->assets_css; + } + + /** + * Return the array of all the registered JS assets + * If a $key is provided, it will try to return only that asset + * else it will return null + * + * @param string|null $key the asset key + * @return array + */ + public function getJs($key = null) + { + if (null !== $key) { + $asset_key = md5($key); + + return $this->assets_js[$asset_key] ?? null; + } + + return $this->assets_js; + } + + /** + * Set the whole array of CSS assets + * + * @param array $css + * @return $this + */ + public function setCss($css) + { + $this->assets_css = $css; + + return $this; + } + + /** + * Set the whole array of JS assets + * + * @param array $js + * @return $this + */ + public function setJs($js) + { + $this->assets_js = $js; + + return $this; + } + + /** + * Removes an item from the CSS array if set + * + * @param string $key The asset key + * @return $this + */ + public function removeCss($key) + { + $asset_key = md5($key); + if (isset($this->assets_css[$asset_key])) { + unset($this->assets_css[$asset_key]); + } + + return $this; + } + + /** + * Removes an item from the JS array if set + * + * @param string $key The asset key + * @return $this + */ + public function removeJs($key) + { + $asset_key = md5($key); + if (isset($this->assets_js[$asset_key])) { + unset($this->assets_js[$asset_key]); + } + + return $this; + } + + /** + * Sets the state of CSS Pipeline + * + * @param bool $value + * @return $this + */ + public function setCssPipeline($value) + { + $this->css_pipeline = (bool)$value; + + return $this; + } + + /** + * Sets the state of JS Pipeline + * + * @param bool $value + * @return $this + */ + public function setJsPipeline($value) + { + $this->js_pipeline = (bool)$value; + + return $this; + } + + /** + * Reset all assets. + * + * @return $this + */ + public function reset() + { + $this->resetCss(); + $this->resetJs(); + $this->setCssPipeline(false); + $this->setJsPipeline(false); + $this->order = []; + + return $this; + } + + /** + * Reset JavaScript assets. + * + * @return $this + */ + public function resetJs() + { + $this->assets_js = []; + + return $this; + } + + /** + * Reset CSS assets. + * + * @return $this + */ + public function resetCss() + { + $this->assets_css = []; + + return $this; + } + + /** + * Explicitly set's a timestamp for assets + * + * @param string|int $value + */ + public function setTimestamp($value) + { + $this->timestamp = $value; + } + + /** + * Get the timestamp for assets + * + * @param bool $include_join + * @return string|null + */ + public function getTimestamp($include_join = true) + { + if ($this->timestamp) { + return $include_join ? '?' . $this->timestamp : $this->timestamp; + } + + return null; + } + + /** + * Add all assets matching $pattern within $directory. + * + * @param string $directory Relative to the Grav root path, or a stream identifier + * @param string $pattern (regex) + * @return $this + */ + public function addDir($directory, $pattern = self::DEFAULT_REGEX) + { + $root_dir = GRAV_ROOT; + + // Check if $directory is a stream. + if (strpos($directory, '://')) { + $directory = Grav::instance()['locator']->findResource($directory, null); + } + + // Get files + $files = $this->rglob($root_dir . DIRECTORY_SEPARATOR . $directory, $pattern, $root_dir . '/'); + + // No luck? Nothing to do + if (!$files) { + return $this; + } + + // Add CSS files + if ($pattern === self::CSS_REGEX) { + foreach ($files as $file) { + $this->addCss($file); + } + + return $this; + } + + // Add JavaScript files + if ($pattern === self::JS_REGEX) { + foreach ($files as $file) { + $this->addJs($file); + } + + return $this; + } + + // Unknown pattern. + foreach ($files as $asset) { + $this->add($asset); + } + + return $this; + } + + /** + * Add all JavaScript assets within $directory + * + * @param string $directory Relative to the Grav root path, or a stream identifier + * @return $this + */ + public function addDirJs($directory) + { + return $this->addDir($directory, self::JS_REGEX); + } + + /** + * Add all CSS assets within $directory + * + * @param string $directory Relative to the Grav root path, or a stream identifier + * @return $this + */ + public function addDirCss($directory) + { + return $this->addDir($directory, self::CSS_REGEX); + } + + /** + * Recursively get files matching $pattern within $directory. + * + * @param string $directory + * @param string $pattern (regex) + * @param string|null $ltrim Will be trimmed from the left of the file path + * @return array + */ + protected function rglob($directory, $pattern, $ltrim = null) + { + $iterator = new RegexIterator(new RecursiveIteratorIterator(new RecursiveDirectoryIterator( + $directory, + FilesystemIterator::SKIP_DOTS + )), $pattern); + $offset = strlen($ltrim); + $files = []; + + foreach ($iterator as $file) { + $files[] = substr($file->getPathname(), $offset); + } + + return $files; + } +} diff --git a/system/src/Grav/Common/Backup/Backups.php b/system/src/Grav/Common/Backup/Backups.php new file mode 100644 index 0000000..4680f85 --- /dev/null +++ b/system/src/Grav/Common/Backup/Backups.php @@ -0,0 +1,322 @@ +addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']); + + $grav->fireEvent('onBackupsInitialized', new Event(['backups' => $this])); + } + + /** + * @return void + */ + public function setup() + { + if (null === static::$backup_dir) { + $grav = Grav::instance(); + static::$backup_dir = $grav['locator']->findResource('backup://', true, true); + Folder::create(static::$backup_dir); + } + } + + /** + * @param Event $event + * @return void + */ + public function onSchedulerInitialized(Event $event) + { + $grav = Grav::instance(); + + /** @var Scheduler $scheduler */ + $scheduler = $event['scheduler']; + + /** @var Inflector $inflector */ + $inflector = $grav['inflector']; + + foreach (static::getBackupProfiles() as $id => $profile) { + $at = $profile['schedule_at']; + $name = $inflector::hyphenize($profile['name']); + $logs = 'logs/backup-' . $name . '.out'; + /** @var Job $job */ + $job = $scheduler->addFunction('Grav\Common\Backup\Backups::backup', [$id], $name); + $job->at($at); + $job->output($logs); + $job->backlink('/tools/backups'); + } + } + + /** + * @param string $backup + * @param string $base_url + * @return string + */ + public function getBackupDownloadUrl($backup, $base_url) + { + $param_sep = $param_sep = Grav::instance()['config']->get('system.param_sep', ':'); + $download = urlencode(base64_encode(basename($backup))); + $url = rtrim(Grav::instance()['uri']->rootUrl(true), '/') . '/' . trim( + $base_url, + '/' + ) . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form'); + + return $url; + } + + /** + * @return array + */ + public static function getBackupProfiles() + { + return Grav::instance()['config']->get('backups.profiles'); + } + + /** + * @return array + */ + public static function getPurgeConfig() + { + return Grav::instance()['config']->get('backups.purge'); + } + + /** + * @return array + */ + public function getBackupNames() + { + return array_column(static::getBackupProfiles(), 'name'); + } + + /** + * @return float|int + */ + public static function getTotalBackupsSize() + { + $backups = static::getAvailableBackups(); + + return array_sum(array_column($backups, 'size')); + } + + /** + * @param bool $force + * @return array + */ + public static function getAvailableBackups($force = false) + { + if ($force || null === static::$backups) { + static::$backups = []; + + $grav = Grav::instance(); + $backups_itr = new GlobIterator(static::$backup_dir . '/*.zip', FilesystemIterator::KEY_AS_FILENAME); + $inflector = $grav['inflector']; + $long_date_format = DATE_RFC2822; + + /** + * @var string $name + * @var SplFileInfo $file + */ + foreach ($backups_itr as $name => $file) { + if (preg_match(static::BACKUP_FILENAME_REGEXZ, $name, $matches)) { + $date = DateTime::createFromFormat(static::BACKUP_DATE_FORMAT, $matches[2]); + $timestamp = $date->getTimestamp(); + $backup = new stdClass(); + $backup->title = $inflector->titleize($matches[1]); + $backup->time = $date; + $backup->date = $date->format($long_date_format); + $backup->filename = $name; + $backup->path = $file->getPathname(); + $backup->size = $file->getSize(); + static::$backups[$timestamp] = $backup; + } + } + // Reverse Key Sort to get in reverse date order + krsort(static::$backups); + } + + return static::$backups; + } + + /** + * Backup + * + * @param int $id + * @param callable|null $status + * @return string|null + */ + public static function backup($id = 0, callable $status = null) + { + $grav = Grav::instance(); + + $profiles = static::getBackupProfiles(); + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + if (isset($profiles[$id])) { + $backup = (object) $profiles[$id]; + } else { + throw new RuntimeException('No backups defined...'); + } + + $name = $grav['inflector']->underscorize($backup->name); + $date = date(static::BACKUP_DATE_FORMAT, time()); + $filename = trim($name, '_') . '--' . $date . '.zip'; + $destination = static::$backup_dir . DS . $filename; + $max_execution_time = ini_set('max_execution_time', '600'); + $backup_root = $backup->root; + + if ($locator->isStream($backup_root)) { + $backup_root = $locator->findResource($backup_root); + } else { + $backup_root = rtrim(GRAV_ROOT . $backup_root, '/'); + } + + if (!$backup_root || !file_exists($backup_root)) { + throw new RuntimeException("Backup location: {$backup_root} does not exist..."); + } + + $options = [ + 'exclude_files' => static::convertExclude($backup->exclude_files ?? ''), + 'exclude_paths' => static::convertExclude($backup->exclude_paths ?? ''), + ]; + + $archiver = Archiver::create('zip'); + $archiver->setArchive($destination)->setOptions($options)->compress($backup_root, $status)->addEmptyFolders($options['exclude_paths'], $status); + + $status && $status([ + 'type' => 'message', + 'message' => 'Done...', + ]); + + $status && $status([ + 'type' => 'progress', + 'complete' => true + ]); + + if ($max_execution_time !== false) { + ini_set('max_execution_time', $max_execution_time); + } + + // Log the backup + $grav['log']->notice('Backup Created: ' . $destination); + + // Fire Finished event + $grav->fireEvent('onBackupFinished', new Event(['backup' => $destination])); + + // Purge anything required + static::purge(); + + // Log + $log = JsonFile::instance($locator->findResource("log://backup.log", true, true)); + $log->content([ + 'time' => time(), + 'location' => $destination + ]); + $log->save(); + + return $destination; + } + + /** + * @return void + * @throws Exception + */ + public static function purge() + { + $purge_config = static::getPurgeConfig(); + $trigger = $purge_config['trigger']; + $backups = static::getAvailableBackups(true); + + switch ($trigger) { + case 'number': + $backups_count = count($backups); + if ($backups_count > $purge_config['max_backups_count']) { + $last = end($backups); + unlink($last->path); + static::purge(); + } + break; + + case 'time': + $last = end($backups); + $now = new DateTime(); + $interval = $now->diff($last->time); + if ($interval->days > $purge_config['max_backups_time']) { + unlink($last->path); + static::purge(); + } + break; + + default: + $used_space = static::getTotalBackupsSize(); + $max_space = $purge_config['max_backups_space'] * 1024 * 1024 * 1024; + if ($used_space > $max_space) { + $last = end($backups); + unlink($last->path); + static::purge(); + } + break; + } + } + + /** + * @param string $exclude + * @return array + */ + protected static function convertExclude($exclude) + { + $lines = preg_split("/[\s,]+/", $exclude); + + return array_map('trim', $lines, array_fill(0, count($lines), '/')); + } +} diff --git a/system/src/Grav/Common/Browser.php b/system/src/Grav/Common/Browser.php new file mode 100644 index 0000000..e4a6513 --- /dev/null +++ b/system/src/Grav/Common/Browser.php @@ -0,0 +1,153 @@ +useragent = parse_user_agent(); + } catch (InvalidArgumentException $e) { + $this->useragent = parse_user_agent("Mozilla/5.0 (compatible; Unknown;)"); + } + } + + /** + * Get the current browser identifier + * + * Currently detected browsers: + * + * Android Browser + * BlackBerry Browser + * Camino + * Kindle / Silk + * Firefox / Iceweasel + * Safari + * Internet Explorer + * IEMobile + * Chrome + * Opera + * Midori + * Vivaldi + * TizenBrowser + * Lynx + * Wget + * Curl + * + * @return string the lowercase browser name + */ + public function getBrowser() + { + return strtolower($this->useragent['browser']); + } + + /** + * Get the current platform identifier + * + * Currently detected platforms: + * + * Desktop + * -> Windows + * -> Linux + * -> Macintosh + * -> Chrome OS + * Mobile + * -> Android + * -> iPhone + * -> iPad / iPod Touch + * -> Windows Phone OS + * -> Kindle + * -> Kindle Fire + * -> BlackBerry + * -> Playbook + * -> Tizen + * Console + * -> Nintendo 3DS + * -> New Nintendo 3DS + * -> Nintendo Wii + * -> Nintendo WiiU + * -> PlayStation 3 + * -> PlayStation 4 + * -> PlayStation Vita + * -> Xbox 360 + * -> Xbox One + * + * @return string the lowercase platform name + */ + public function getPlatform() + { + return strtolower($this->useragent['platform']); + } + + /** + * Get the current full version identifier + * + * @return string the browser full version identifier + */ + public function getLongVersion() + { + return $this->useragent['version']; + } + + /** + * Get the current major version identifier + * + * @return int the browser major version identifier + */ + public function getVersion() + { + $version = explode('.', $this->getLongVersion()); + + return (int)$version[0]; + } + + /** + * Determine if the request comes from a human, or from a bot/crawler + * + * @return bool + */ + public function isHuman() + { + $browser = $this->getBrowser(); + if (empty($browser)) { + return false; + } + + if (preg_match('~(bot|crawl)~i', $browser)) { + return false; + } + + return true; + } + + /** + * Determine if “Do Not Track” is set by browser + * @see https://www.w3.org/TR/tracking-dnt/ + * + * @return bool + */ + public function isTrackable(): bool + { + return !(isset($_SERVER['HTTP_DNT']) && $_SERVER['HTTP_DNT'] === '1'); + } +} diff --git a/system/src/Grav/Common/Cache.php b/system/src/Grav/Common/Cache.php new file mode 100644 index 0000000..a503518 --- /dev/null +++ b/system/src/Grav/Common/Cache.php @@ -0,0 +1,690 @@ +init($grav); + } + + /** + * Initialization that sets a base key and the driver based on configuration settings + * + * @param Grav $grav + * @return void + */ + public function init(Grav $grav) + { + $this->config = $grav['config']; + $this->now = time(); + + if (null === $this->enabled) { + $this->enabled = (bool)$this->config->get('system.cache.enabled'); + } + + /** @var Uri $uri */ + $uri = $grav['uri']; + + $prefix = $this->config->get('system.cache.prefix'); + $uniqueness = substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), 2, 8); + + // Cache key allows us to invalidate all cache on configuration changes. + $this->key = ($prefix ?: 'g') . '-' . $uniqueness; + $this->cache_dir = $grav['locator']->findResource('cache://doctrine/' . $uniqueness, true, true); + $this->driver_setting = $this->config->get('system.cache.driver'); + $this->driver = $this->getCacheDriver(); + $this->driver->setNamespace($this->key); + + /** @var EventDispatcher $dispatcher */ + $dispatcher = Grav::instance()['events']; + $dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']); + } + + /** + * @return CacheInterface + */ + public function getSimpleCache() + { + if (null === $this->simpleCache) { + $cache = new \Grav\Framework\Cache\Adapter\DoctrineCache($this->driver, '', $this->getLifetime()); + + // Disable cache key validation. + $cache->setValidation(false); + + $this->simpleCache = $cache; + } + + return $this->simpleCache; + } + + /** + * Deletes the old out of date file-based caches + * + * @return int + */ + public function purgeOldCache() + { + $cache_dir = dirname($this->cache_dir); + $current = basename($this->cache_dir); + $count = 0; + + foreach (new DirectoryIterator($cache_dir) as $file) { + $dir = $file->getBasename(); + if ($dir === $current || $file->isDot() || $file->isFile()) { + continue; + } + + Folder::delete($file->getPathname()); + $count++; + } + + return $count; + } + + /** + * Public accessor to set the enabled state of the cache + * + * @param bool|int $enabled + * @return void + */ + public function setEnabled($enabled) + { + $this->enabled = (bool)$enabled; + } + + /** + * Returns the current enabled state + * + * @return bool + */ + public function getEnabled() + { + return $this->enabled; + } + + /** + * Get cache state + * + * @return string + */ + public function getCacheStatus() + { + return 'Cache: [' . ($this->enabled ? 'true' : 'false') . '] Setting: [' . $this->driver_setting . '] Driver: [' . $this->driver_name . ']'; + } + + /** + * Automatically picks the cache mechanism to use. If you pick one manually it will use that + * If there is no config option for $driver in the config, or it's set to 'auto', it will + * pick the best option based on which cache extensions are installed. + * + * @return DoctrineCache\CacheProvider The cache driver to use + */ + public function getCacheDriver() + { + $setting = $this->driver_setting; + $driver_name = 'file'; + + // CLI compatibility requires a non-volatile cache driver + if ($this->config->get('system.cache.cli_compatibility') && ( + $setting === 'auto' || $this->isVolatileDriver($setting))) { + $setting = $driver_name; + } + + if (!$setting || $setting === 'auto') { + if (extension_loaded('apcu')) { + $driver_name = 'apcu'; + } elseif (extension_loaded('wincache')) { + $driver_name = 'wincache'; + } + } else { + $driver_name = $setting; + } + + $this->driver_name = $driver_name; + + switch ($driver_name) { + case 'apc': + case 'apcu': + $driver = new DoctrineCache\ApcuCache(); + break; + + case 'wincache': + $driver = new DoctrineCache\WinCacheCache(); + break; + + case 'memcache': + if (extension_loaded('memcache')) { + $memcache = new \Memcache(); + $memcache->connect( + $this->config->get('system.cache.memcache.server', 'localhost'), + $this->config->get('system.cache.memcache.port', 11211) + ); + $driver = new DoctrineCache\MemcacheCache(); + $driver->setMemcache($memcache); + } else { + throw new LogicException('Memcache PHP extension has not been installed'); + } + break; + + case 'memcached': + if (extension_loaded('memcached')) { + $memcached = new \Memcached(); + $memcached->addServer( + $this->config->get('system.cache.memcached.server', 'localhost'), + $this->config->get('system.cache.memcached.port', 11211) + ); + $driver = new DoctrineCache\MemcachedCache(); + $driver->setMemcached($memcached); + } else { + throw new LogicException('Memcached PHP extension has not been installed'); + } + break; + + case 'redis': + if (extension_loaded('redis')) { + $redis = new \Redis(); + $socket = $this->config->get('system.cache.redis.socket', false); + $password = $this->config->get('system.cache.redis.password', false); + $databaseId = $this->config->get('system.cache.redis.database', 0); + + if ($socket) { + $redis->connect($socket); + } else { + $redis->connect( + $this->config->get('system.cache.redis.server', 'localhost'), + $this->config->get('system.cache.redis.port', 6379) + ); + } + + // Authenticate with password if set + if ($password && !$redis->auth($password)) { + throw new \RedisException('Redis authentication failed'); + } + + // Select alternate ( !=0 ) database ID if set + if ($databaseId && !$redis->select($databaseId)) { + throw new \RedisException('Could not select alternate Redis database ID'); + } + + $driver = new DoctrineCache\RedisCache(); + $driver->setRedis($redis); + } else { + throw new LogicException('Redis PHP extension has not been installed'); + } + break; + + default: + $driver = new DoctrineCache\FilesystemCache($this->cache_dir); + break; + } + + return $driver; + } + + /** + * Gets a cached entry if it exists based on an id. If it does not exist, it returns false + * + * @param string $id the id of the cached entry + * @return mixed|bool returns the cached entry, can be any type, or false if doesn't exist + */ + public function fetch($id) + { + if ($this->enabled) { + return $this->driver->fetch($id); + } + + return false; + } + + /** + * Stores a new cached entry. + * + * @param string $id the id of the cached entry + * @param array|object|int $data the data for the cached entry to store + * @param int|null $lifetime the lifetime to store the entry in seconds + */ + public function save($id, $data, $lifetime = null) + { + if ($this->enabled) { + if ($lifetime === null) { + $lifetime = $this->getLifetime(); + } + $this->driver->save($id, $data, $lifetime); + } + } + + /** + * Deletes an item in the cache based on the id + * + * @param string $id the id of the cached data entry + * @return bool true if the item was deleted successfully + */ + public function delete($id) + { + if ($this->enabled) { + return $this->driver->delete($id); + } + + return false; + } + + /** + * Deletes all cache + * + * @return bool + */ + public function deleteAll() + { + if ($this->enabled) { + return $this->driver->deleteAll(); + } + + return false; + } + + /** + * Returns a boolean state of whether or not the item exists in the cache based on id key + * + * @param string $id the id of the cached data entry + * @return bool true if the cached items exists + */ + public function contains($id) + { + if ($this->enabled) { + return $this->driver->contains(($id)); + } + + return false; + } + + /** + * Getter method to get the cache key + * + * @return string + */ + public function getKey() + { + return $this->key; + } + + /** + * Setter method to set key (Advanced) + * + * @param string $key + * @return void + */ + public function setKey($key) + { + $this->key = $key; + $this->driver->setNamespace($this->key); + } + + /** + * Helper method to clear all Grav caches + * + * @param string $remove standard|all|assets-only|images-only|cache-only + * @return array + */ + public static function clearCache($remove = 'standard') + { + $locator = Grav::instance()['locator']; + $output = []; + $user_config = USER_DIR . 'config/system.yaml'; + + switch ($remove) { + case 'all': + $remove_paths = self::$all_remove; + break; + case 'assets-only': + $remove_paths = self::$assets_remove; + break; + case 'images-only': + $remove_paths = self::$images_remove; + break; + case 'cache-only': + $remove_paths = self::$cache_remove; + break; + case 'tmp-only': + $remove_paths = self::$tmp_remove; + break; + case 'invalidate': + $remove_paths = []; + break; + default: + if (Grav::instance()['config']->get('system.cache.clear_images_by_default')) { + $remove_paths = self::$standard_remove; + } else { + $remove_paths = self::$standard_remove_no_images; + } + } + + // Delete entries in the doctrine cache if required + if (in_array($remove, ['all', 'standard'])) { + $cache = Grav::instance()['cache']; + $cache->driver->deleteAll(); + } + + // Clearing cache event to add paths to clear + Grav::instance()->fireEvent('onBeforeCacheClear', new Event(['remove' => $remove, 'paths' => &$remove_paths])); + + foreach ($remove_paths as $stream) { + // Convert stream to a real path + try { + $path = $locator->findResource($stream, true, true); + if ($path === false) { + continue; + } + + $anything = false; + $files = glob($path . '/*'); + + if (is_array($files)) { + foreach ($files as $file) { + if (is_link($file)) { + $output[] = 'Skipping symlink: ' . $file; + } elseif (is_file($file)) { + if (@unlink($file)) { + $anything = true; + } + } elseif (is_dir($file)) { + if (Folder::delete($file, false)) { + $anything = true; + } + } + } + } + + if ($anything) { + $output[] = 'Cleared: ' . $path . '/*'; + } + } catch (Exception $e) { + // stream not found or another error while deleting files. + $output[] = 'ERROR: ' . $e->getMessage(); + } + } + + $output[] = ''; + + if (($remove === 'all' || $remove === 'standard') && file_exists($user_config)) { + touch($user_config); + + $output[] = 'Touched: ' . $user_config; + $output[] = ''; + } + + // Clear stat cache + @clearstatcache(); + + // Clear opcache + if (function_exists('opcache_reset')) { + @opcache_reset(); + } + + Grav::instance()->fireEvent('onAfterCacheClear', new Event(['remove' => $remove, 'output' => &$output])); + + return $output; + } + + /** + * @return void + */ + public static function invalidateCache() + { + $user_config = USER_DIR . 'config/system.yaml'; + + if (file_exists($user_config)) { + touch($user_config); + } + + // Clear stat cache + @clearstatcache(); + + // Clear opcache + if (function_exists('opcache_reset')) { + @opcache_reset(); + } + } + + /** + * Set the cache lifetime programmatically + * + * @param int $future timestamp + * @return void + */ + public function setLifetime($future) + { + if (!$future) { + return; + } + + $interval = (int)($future - $this->now); + if ($interval > 0 && $interval < $this->getLifetime()) { + $this->lifetime = $interval; + } + } + + + /** + * Retrieve the cache lifetime (in seconds) + * + * @return int + */ + public function getLifetime() + { + if ($this->lifetime === null) { + $this->lifetime = (int)($this->config->get('system.cache.lifetime') ?: 604800); // 1 week default + } + + return $this->lifetime; + } + + /** + * Returns the current driver name + * + * @return string + */ + public function getDriverName() + { + return $this->driver_name; + } + + /** + * Returns the current driver setting + * + * @return string + */ + public function getDriverSetting() + { + return $this->driver_setting; + } + + /** + * is this driver a volatile driver in that it resides in PHP process memory + * + * @param string $setting + * @return bool + */ + public function isVolatileDriver($setting) + { + return in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'], true); + } + + /** + * Static function to call as a scheduled Job to purge old Doctrine files + * + * @param bool $echo + * + * @return string|void + */ + public static function purgeJob($echo = false) + { + /** @var Cache $cache */ + $cache = Grav::instance()['cache']; + $deleted_folders = $cache->purgeOldCache(); + $msg = 'Purged ' . $deleted_folders . ' old cache folders...'; + + if ($echo) { + echo $msg; + } else { + return $msg; + } + } + + /** + * Static function to call as a scheduled Job to clear Grav cache + * + * @param string $type + * @return void + */ + public static function clearJob($type) + { + $result = static::clearCache($type); + static::invalidateCache(); + + echo strip_tags(implode("\n", $result)); + } + + /** + * @param Event $event + * @return void + */ + public function onSchedulerInitialized(Event $event) + { + /** @var Scheduler $scheduler */ + $scheduler = $event['scheduler']; + $config = Grav::instance()['config']; + + // File Cache Purge + $at = $config->get('system.cache.purge_at'); + $name = 'cache-purge'; + $logs = 'logs/' . $name . '.out'; + + $job = $scheduler->addFunction('Grav\Common\Cache::purgeJob', [true], $name); + $job->at($at); + $job->output($logs); + $job->backlink('/config/system#caching'); + + // Cache Clear + $at = $config->get('system.cache.clear_at'); + $clear_type = $config->get('system.cache.clear_job_type'); + $name = 'cache-clear'; + $logs = 'logs/' . $name . '.out'; + + $job = $scheduler->addFunction('Grav\Common\Cache::clearJob', [$clear_type], $name); + $job->at($at); + $job->output($logs); + $job->backlink('/config/system#caching'); + } +} diff --git a/system/src/Grav/Common/Composer.php b/system/src/Grav/Common/Composer.php new file mode 100644 index 0000000..f1f6e5d --- /dev/null +++ b/system/src/Grav/Common/Composer.php @@ -0,0 +1,67 @@ +path = $path ? rtrim($path, '\\/') . '/' : ''; + $this->cacheFolder = $cacheFolder; + $this->files = $files; + } + + /** + * Get filename for the compiled PHP file. + * + * @param string|null $name + * @return $this + */ + public function name($name = null) + { + if (!$this->name) { + $this->name = $name ?: md5(json_encode(array_keys($this->files))); + } + + return $this; + } + + /** + * Function gets called when cached configuration is saved. + * + * @return void + */ + public function modified() + { + } + + /** + * Get timestamp of compiled configuration + * + * @return int Timestamp of compiled configuration + */ + public function timestamp() + { + return $this->timestamp ?: time(); + } + + /** + * Load the configuration. + * + * @return mixed + */ + public function load() + { + if ($this->object) { + return $this->object; + } + + $filename = $this->createFilename(); + if (!$this->loadCompiledFile($filename) && $this->loadFiles()) { + $this->saveCompiledFile($filename); + } + + return $this->object; + } + + /** + * Returns checksum from the configuration files. + * + * You can set $this->checksum = false to disable this check. + * + * @return bool|string + */ + public function checksum() + { + if (null === $this->checksum) { + $this->checksum = md5(json_encode($this->files) . $this->version); + } + + return $this->checksum; + } + + /** + * @return string + */ + protected function createFilename() + { + return "{$this->cacheFolder}/{$this->name()->name}.php"; + } + + /** + * Create configuration object. + * + * @param array $data + * @return void + */ + abstract protected function createObject(array $data = []); + + /** + * Finalize configuration object. + * + * @return void + */ + abstract protected function finalizeObject(); + + /** + * Load single configuration file and append it to the correct position. + * + * @param string $name Name of the position. + * @param string|string[] $filename File(s) to be loaded. + * @return void + */ + abstract protected function loadFile($name, $filename); + + /** + * Load and join all configuration files. + * + * @return bool + * @internal + */ + protected function loadFiles() + { + $this->createObject(); + + $list = array_reverse($this->files); + foreach ($list as $files) { + foreach ($files as $name => $item) { + $this->loadFile($name, $this->path . $item['file']); + } + } + + $this->finalizeObject(); + + return true; + } + + /** + * Load compiled file. + * + * @param string $filename + * @return bool + * @internal + */ + protected function loadCompiledFile($filename) + { + if (!file_exists($filename)) { + return false; + } + + $cache = include $filename; + if (!is_array($cache) + || !isset($cache['checksum'], $cache['data'], $cache['@class']) + || $cache['@class'] !== get_class($this) + ) { + return false; + } + + // Load real file if cache isn't up to date (or is invalid). + if ($cache['checksum'] !== $this->checksum()) { + return false; + } + + $this->createObject($cache['data']); + $this->timestamp = $cache['timestamp'] ?? 0; + + $this->finalizeObject(); + + return true; + } + + /** + * Save compiled file. + * + * @param string $filename + * @return void + * @throws RuntimeException + * @internal + */ + protected function saveCompiledFile($filename) + { + $file = PhpFile::instance($filename); + + // Attempt to lock the file for writing. + try { + $file->lock(false); + } catch (Exception $e) { + // Another process has locked the file; we will check this in a bit. + } + + if ($file->locked() === false) { + // File was already locked by another process. + return; + } + + $cache = [ + '@class' => get_class($this), + 'timestamp' => time(), + 'checksum' => $this->checksum(), + 'files' => $this->files, + 'data' => $this->getState() + ]; + + $file->save($cache); + $file->unlock(); + $file->free(); + + $this->modified(); + } + + /** + * @return array + */ + protected function getState() + { + return $this->object->toArray(); + } +} diff --git a/system/src/Grav/Common/Config/CompiledBlueprints.php b/system/src/Grav/Common/Config/CompiledBlueprints.php new file mode 100644 index 0000000..7743054 --- /dev/null +++ b/system/src/Grav/Common/Config/CompiledBlueprints.php @@ -0,0 +1,131 @@ +version = 2; + } + + /** + * Returns checksum from the configuration files. + * + * You can set $this->checksum = false to disable this check. + * + * @return bool|string + */ + public function checksum() + { + if (null === $this->checksum) { + $this->checksum = md5(json_encode($this->files) . json_encode($this->getTypes()) . $this->version); + } + + return $this->checksum; + } + + /** + * Create configuration object. + * + * @param array $data + */ + protected function createObject(array $data = []) + { + $this->object = (new BlueprintSchema($data))->setTypes($this->getTypes()); + } + + /** + * Get list of form field types. + * + * @return array + */ + protected function getTypes() + { + return Grav::instance()['plugins']->formFieldTypes ?: []; + } + + /** + * Finalize configuration object. + * + * @return void + */ + protected function finalizeObject() + { + } + + /** + * Load single configuration file and append it to the correct position. + * + * @param string $name Name of the position. + * @param array $files Files to be loaded. + * @return void + */ + protected function loadFile($name, $files) + { + // Load blueprint file. + $blueprint = new Blueprint($files); + + $this->object->embed($name, $blueprint->load()->toArray(), '/', true); + } + + /** + * Load and join all configuration files. + * + * @return bool + * @internal + */ + protected function loadFiles() + { + $this->createObject(); + + // Convert file list into parent list. + $list = []; + /** @var array $files */ + foreach ($this->files as $files) { + foreach ($files as $name => $item) { + $list[$name][] = $this->path . $item['file']; + } + } + + // Load files. + foreach ($list as $name => $files) { + $this->loadFile($name, $files); + } + + $this->finalizeObject(); + + return true; + } + + /** + * @return array + */ + protected function getState() + { + return $this->object->getState(); + } +} diff --git a/system/src/Grav/Common/Config/CompiledConfig.php b/system/src/Grav/Common/Config/CompiledConfig.php new file mode 100644 index 0000000..22225bc --- /dev/null +++ b/system/src/Grav/Common/Config/CompiledConfig.php @@ -0,0 +1,114 @@ +version = 1; + } + + /** + * Set blueprints for the configuration. + * + * @param callable $blueprints + * @return $this + */ + public function setBlueprints(callable $blueprints) + { + $this->callable = $blueprints; + + return $this; + } + + /** + * @param bool $withDefaults + * @return mixed + */ + public function load($withDefaults = false) + { + $this->withDefaults = $withDefaults; + + return parent::load(); + } + + /** + * Create configuration object. + * + * @param array $data + * @return void + */ + protected function createObject(array $data = []) + { + if ($this->withDefaults && empty($data) && is_callable($this->callable)) { + $blueprints = $this->callable; + $data = $blueprints()->getDefaults(); + } + + $this->object = new Config($data, $this->callable); + } + + /** + * Finalize configuration object. + * + * @return void + */ + protected function finalizeObject() + { + $this->object->checksum($this->checksum()); + $this->object->timestamp($this->timestamp()); + } + + /** + * Function gets called when cached configuration is saved. + * + * @return void + */ + public function modified() + { + $this->object->modified(true); + } + + /** + * Load single configuration file and append it to the correct position. + * + * @param string $name Name of the position. + * @param string $filename File to be loaded. + * @return void + */ + protected function loadFile($name, $filename) + { + $file = CompiledYamlFile::instance($filename); + $this->object->join($name, $file->content(), '/'); + $file->free(); + } +} diff --git a/system/src/Grav/Common/Config/CompiledLanguages.php b/system/src/Grav/Common/Config/CompiledLanguages.php new file mode 100644 index 0000000..6674389 --- /dev/null +++ b/system/src/Grav/Common/Config/CompiledLanguages.php @@ -0,0 +1,83 @@ +version = 1; + } + + /** + * Create configuration object. + * + * @param array $data + * @return void + */ + protected function createObject(array $data = []) + { + $this->object = new Languages($data); + } + + /** + * Finalize configuration object. + * + * @return void + */ + protected function finalizeObject() + { + $this->object->checksum($this->checksum()); + $this->object->timestamp($this->timestamp()); + } + + + /** + * Function gets called when cached configuration is saved. + * + * @return void + */ + public function modified() + { + $this->object->modified(true); + } + + /** + * Load single configuration file and append it to the correct position. + * + * @param string $name Name of the position. + * @param string $filename File to be loaded. + * @return void + */ + protected function loadFile($name, $filename) + { + $file = CompiledYamlFile::instance($filename); + if (preg_match('|languages\.yaml$|', $filename)) { + $this->object->mergeRecursive((array) $file->content()); + } else { + $this->object->mergeRecursive([$name => $file->content()]); + } + $file->free(); + } +} diff --git a/system/src/Grav/Common/Config/Config.php b/system/src/Grav/Common/Config/Config.php new file mode 100644 index 0000000..7adb523 --- /dev/null +++ b/system/src/Grav/Common/Config/Config.php @@ -0,0 +1,156 @@ +key) { + $this->key = md5($this->checksum . $this->timestamp); + } + + return $this->key; + } + + /** + * @param string|null $checksum + * @return string|null + */ + public function checksum($checksum = null) + { + if ($checksum !== null) { + $this->checksum = $checksum; + } + + return $this->checksum; + } + + /** + * @param bool|null $modified + * @return bool + */ + public function modified($modified = null) + { + if ($modified !== null) { + $this->modified = $modified; + } + + return $this->modified; + } + + /** + * @param int|null $timestamp + * @return int + */ + public function timestamp($timestamp = null) + { + if ($timestamp !== null) { + $this->timestamp = $timestamp; + } + + return $this->timestamp; + } + + /** + * @return $this + */ + public function reload() + { + $grav = Grav::instance(); + + // Load new configuration. + $config = ConfigServiceProvider::load($grav); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + + if ($config->modified()) { + // Update current configuration. + $this->items = $config->toArray(); + $this->checksum($config->checksum()); + $this->modified(true); + + $debugger->addMessage('Configuration was changed and saved.'); + } + + return $this; + } + + /** + * @return void + */ + public function debug() + { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + + $debugger->addMessage('Environment Name: ' . $this->environment); + if ($this->modified()) { + $debugger->addMessage('Configuration reloaded and cached.'); + } + } + + /** + * @return void + */ + public function init() + { + $setup = Grav::instance()['setup']->toArray(); + foreach ($setup as $key => $value) { + if ($key === 'streams' || !is_array($value)) { + // Optimized as streams and simple values are fully defined in setup. + $this->items[$key] = $value; + } else { + $this->joinDefaults($key, $value); + } + } + + // Legacy value - Override the media.upload_limit based on PHP values + $this->items['system']['media']['upload_limit'] = Utils::getUploadLimit(); + } + + /** + * @return mixed + * @deprecated 1.5 Use Grav::instance()['languages'] instead. + */ + public function getLanguages() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use Grav::instance()[\'languages\'] instead', E_USER_DEPRECATED); + + return Grav::instance()['languages']; + } +} diff --git a/system/src/Grav/Common/Config/ConfigFileFinder.php b/system/src/Grav/Common/Config/ConfigFileFinder.php new file mode 100644 index 0000000..e6b1afe --- /dev/null +++ b/system/src/Grav/Common/Config/ConfigFileFinder.php @@ -0,0 +1,273 @@ +base = $base ? "{$base}/" : ''; + + return $this; + } + + /** + * Return all locations for all the files with a timestamp. + * + * @param array $paths List of folders to look from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + */ + public function locateFiles(array $paths, $pattern = '|\.yaml$|', $levels = -1) + { + $list = []; + foreach ($paths as $folder) { + $list += $this->detectRecursive($folder, $pattern, $levels); + } + + return $list; + } + + /** + * Return all locations for all the files with a timestamp. + * + * @param array $paths List of folders to look from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + */ + public function getFiles(array $paths, $pattern = '|\.yaml$|', $levels = -1) + { + $list = []; + foreach ($paths as $folder) { + $path = trim(Folder::getRelativePath($folder), '/'); + + $files = $this->detectRecursive($folder, $pattern, $levels); + + $list += $files[trim($path, '/')]; + } + + return $list; + } + + /** + * Return all paths for all the files with a timestamp. + * + * @param array $paths List of folders to look from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + */ + public function listFiles(array $paths, $pattern = '|\.yaml$|', $levels = -1) + { + $list = []; + foreach ($paths as $folder) { + $list = array_merge_recursive($list, $this->detectAll($folder, $pattern, $levels)); + } + + return $list; + } + + /** + * Find filename from a list of folders. + * + * Note: Only finds the last override. + * + * @param string $filename + * @param array $folders + * @return array + */ + public function locateFileInFolder($filename, array $folders) + { + $list = []; + foreach ($folders as $folder) { + $list += $this->detectInFolder($folder, $filename); + } + + return $list; + } + + /** + * Find filename from a list of folders. + * + * @param array $folders + * @param string|null $filename + * @return array + */ + public function locateInFolders(array $folders, $filename = null) + { + $list = []; + foreach ($folders as $folder) { + $path = trim(Folder::getRelativePath($folder), '/'); + $list[$path] = $this->detectInFolder($folder, $filename); + } + + return $list; + } + + /** + * Return all existing locations for a single file with a timestamp. + * + * @param array $paths Filesystem paths to look up from. + * @param string $name Configuration file to be located. + * @param string $ext File extension (optional, defaults to .yaml). + * @return array + */ + public function locateFile(array $paths, $name, $ext = '.yaml') + { + $filename = preg_replace('|[.\/]+|', '/', $name) . $ext; + + $list = []; + foreach ($paths as $folder) { + $path = trim(Folder::getRelativePath($folder), '/'); + + if (is_file("{$folder}/{$filename}")) { + $modified = filemtime("{$folder}/{$filename}"); + } else { + $modified = 0; + } + $basename = $this->base . $name; + $list[$path] = [$basename => ['file' => "{$path}/{$filename}", 'modified' => $modified]]; + } + + return $list; + } + + /** + * Detects all directories with a configuration file and returns them with last modification time. + * + * @param string $folder Location to look up from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + * @internal + */ + protected function detectRecursive($folder, $pattern, $levels) + { + $path = trim(Folder::getRelativePath($folder), '/'); + + if (is_dir($folder)) { + // Find all system and user configuration files. + $options = [ + 'levels' => $levels, + 'compare' => 'Filename', + 'pattern' => $pattern, + 'filters' => [ + 'pre-key' => $this->base, + 'key' => $pattern, + 'value' => function (RecursiveDirectoryIterator $file) use ($path) { + return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()]; + } + ], + 'key' => 'SubPathname' + ]; + + $list = Folder::all($folder, $options); + + ksort($list); + } else { + $list = []; + } + + return [$path => $list]; + } + + /** + * Detects all directories with the lookup file and returns them with last modification time. + * + * @param string $folder Location to look up from. + * @param string|null $lookup Filename to be located (defaults to directory name). + * @return array + * @internal + */ + protected function detectInFolder($folder, $lookup = null) + { + $folder = rtrim($folder, '/'); + $path = trim(Folder::getRelativePath($folder), '/'); + $base = $path === $folder ? '' : ($path ? substr($folder, 0, -strlen($path)) : $folder . '/'); + + $list = []; + + if (is_dir($folder)) { + $iterator = new DirectoryIterator($folder); + foreach ($iterator as $directory) { + if (!$directory->isDir() || $directory->isDot()) { + continue; + } + + $name = $directory->getFilename(); + $find = ($lookup ?: $name) . '.yaml'; + $filename = "{$path}/{$name}/{$find}"; + + if (file_exists($base . $filename)) { + $basename = $this->base . $name; + $list[$basename] = ['file' => $filename, 'modified' => filemtime($base . $filename)]; + } + } + } + + return $list; + } + + /** + * Detects all plugins with a configuration file and returns them with last modification time. + * + * @param string $folder Location to look up from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + * @internal + */ + protected function detectAll($folder, $pattern, $levels) + { + $path = trim(Folder::getRelativePath($folder), '/'); + + if (is_dir($folder)) { + // Find all system and user configuration files. + $options = [ + 'levels' => $levels, + 'compare' => 'Filename', + 'pattern' => $pattern, + 'filters' => [ + 'pre-key' => $this->base, + 'key' => $pattern, + 'value' => function (RecursiveDirectoryIterator $file) use ($path) { + return ["{$path}/{$file->getSubPathname()}" => $file->getMTime()]; + } + ], + 'key' => 'SubPathname' + ]; + + $list = Folder::all($folder, $options); + + ksort($list); + } else { + $list = []; + } + + return $list; + } +} diff --git a/system/src/Grav/Common/Config/Languages.php b/system/src/Grav/Common/Config/Languages.php new file mode 100644 index 0000000..4a863a1 --- /dev/null +++ b/system/src/Grav/Common/Config/Languages.php @@ -0,0 +1,107 @@ +checksum = $checksum; + } + + return $this->checksum; + } + + /** + * @param bool|null $modified + * @return bool + */ + public function modified($modified = null) + { + if ($modified !== null) { + $this->modified = $modified; + } + + return $this->modified; + } + + /** + * @param int|null $timestamp + * @return int + */ + public function timestamp($timestamp = null) + { + if ($timestamp !== null) { + $this->timestamp = $timestamp; + } + + return $this->timestamp; + } + + /** + * @return void + */ + public function reformat() + { + if (isset($this->items['plugins'])) { + $this->items = array_merge_recursive($this->items, $this->items['plugins']); + unset($this->items['plugins']); + } + } + + /** + * @param array $data + * @return void + */ + public function mergeRecursive(array $data) + { + $this->items = Utils::arrayMergeRecursiveUnique($this->items, $data); + } + + /** + * @param string $lang + * @return array + */ + public function flattenByLang($lang) + { + $language = $this->items[$lang]; + return Utils::arrayFlattenDotNotation($language); + } + + /** + * @param array $array + * @return array + */ + public function unflatten($array) + { + return Utils::arrayUnflattenDotNotation($array); + } +} diff --git a/system/src/Grav/Common/Config/Setup.php b/system/src/Grav/Common/Config/Setup.php new file mode 100644 index 0000000..6ff7372 --- /dev/null +++ b/system/src/Grav/Common/Config/Setup.php @@ -0,0 +1,422 @@ + 'unknown', + '127.0.0.1' => 'localhost', + '::1' => 'localhost' + ]; + + /** + * @var string|null Current environment normalized to lower case. + */ + public static $environment; + + /** @var string */ + public static $securityFile = 'config://security.yaml'; + + /** @var array */ + protected $streams = [ + 'user' => [ + 'type' => 'ReadOnlyStream', + 'force' => true, + 'prefixes' => [ + '' => [] // Set in constructor + ] + ], + 'cache' => [ + 'type' => 'Stream', + 'force' => true, + 'prefixes' => [ + '' => [], // Set in constructor + 'images' => ['images'] + ] + ], + 'log' => [ + 'type' => 'Stream', + 'force' => true, + 'prefixes' => [ + '' => [] // Set in constructor + ] + ], + 'tmp' => [ + 'type' => 'Stream', + 'force' => true, + 'prefixes' => [ + '' => [] // Set in constructor + ] + ], + 'backup' => [ + 'type' => 'Stream', + 'force' => true, + 'prefixes' => [ + '' => [] // Set in constructor + ] + ], + 'environment' => [ + 'type' => 'ReadOnlyStream' + // If not defined, environment will be set up in the constructor. + ], + 'system' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['system'], + ] + ], + 'asset' => [ + 'type' => 'Stream', + 'prefixes' => [ + '' => ['assets'], + ] + ], + 'blueprints' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['environment://blueprints', 'user://blueprints', 'system://blueprints'], + ] + ], + 'config' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['environment://config', 'user://config', 'system://config'], + ] + ], + 'plugins' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://plugins'], + ] + ], + 'plugin' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://plugins'], + ] + ], + 'themes' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://themes'], + ] + ], + 'languages' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['environment://languages', 'user://languages', 'system://languages'], + ] + ], + 'image' => [ + 'type' => 'Stream', + 'prefixes' => [ + '' => ['user://images', 'system://images'] + ] + ], + 'page' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://pages'] + ] + ], + 'user-data' => [ + 'type' => 'Stream', + 'force' => true, + 'prefixes' => [ + '' => ['user://data'] + ] + ], + 'account' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://accounts'] + ] + ], + ]; + + /** + * @param Container|array $container + */ + public function __construct($container) + { + // Configure main streams. + $abs = str_starts_with(GRAV_SYSTEM_PATH, '/'); + $this->streams['system']['prefixes'][''] = $abs ? ['system', GRAV_SYSTEM_PATH] : ['system']; + $this->streams['user']['prefixes'][''] = [GRAV_USER_PATH]; + $this->streams['cache']['prefixes'][''] = [GRAV_CACHE_PATH]; + $this->streams['log']['prefixes'][''] = [GRAV_LOG_PATH]; + $this->streams['tmp']['prefixes'][''] = [GRAV_TMP_PATH]; + $this->streams['backup']['prefixes'][''] = [GRAV_BACKUP_PATH]; + + // If environment is not set, look for the environment variable and then the constant. + $environment = static::$environment ?? + (defined('GRAV_ENVIRONMENT') ? GRAV_ENVIRONMENT : (getenv('GRAV_ENVIRONMENT') ?: null)); + + // If no environment is set, make sure we get one (CLI or hostname). + if (null === $environment) { + if (defined('GRAV_CLI')) { + $environment = 'cli'; + } else { + /** @var ServerRequestInterface $request */ + $request = $container['request']; + $host = $request->getUri()->getHost(); + + $environment = Utils::substrToString($host, ':'); + } + } + + // Resolve server aliases to the proper environment. + static::$environment = static::$environments[$environment] ?? $environment; + + // Pre-load setup.php which contains our initial configuration. + // Configuration may contain dynamic parts, which is why we need to always load it. + // If GRAV_SETUP_PATH has been defined, use it, otherwise use defaults. + $setupFile = defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH : (getenv('GRAV_SETUP_PATH') ?: null); + if (null !== $setupFile) { + // Make sure that the custom setup file exists. Terminates the script if not. + if (!str_starts_with($setupFile, '/')) { + $setupFile = GRAV_WEBROOT . '/' . $setupFile; + } + if (!is_file($setupFile)) { + echo 'GRAV_SETUP_PATH is defined but does not point to existing setup file.'; + exit(1); + } + } else { + $setupFile = GRAV_WEBROOT . '/setup.php'; + if (!is_file($setupFile)) { + $setupFile = GRAV_WEBROOT . '/' . GRAV_USER_PATH . '/setup.php'; + } + if (!is_file($setupFile)) { + $setupFile = null; + } + } + $setup = $setupFile ? (array) include $setupFile : []; + + // Add default streams defined in beginning of the class. + if (!isset($setup['streams']['schemes'])) { + $setup['streams']['schemes'] = []; + } + $setup['streams']['schemes'] += $this->streams; + + // Initialize class. + parent::__construct($setup); + + $this->def('environment', static::$environment); + + // Figure out path for the current environment. + $envPath = defined('GRAV_ENVIRONMENT_PATH') ? GRAV_ENVIRONMENT_PATH : (getenv('GRAV_ENVIRONMENT_PATH') ?: null); + if (null === $envPath) { + // Find common path for all environments and append current environment into it. + $envPath = defined('GRAV_ENVIRONMENTS_PATH') ? GRAV_ENVIRONMENTS_PATH : (getenv('GRAV_ENVIRONMENTS_PATH') ?: null); + if (null !== $envPath) { + $envPath .= '/'; + } else { + // Use default location. Start with Grav 1.7 default. + $envPath = GRAV_WEBROOT. '/' . GRAV_USER_PATH . '/env'; + if (is_dir($envPath)) { + $envPath = 'user://env/'; + } else { + // Fallback to Grav 1.6 default. + $envPath = 'user://'; + } + } + $envPath .= $this->get('environment'); + } + + // Set up environment. + $this->def('environment', static::$environment); + $this->def('streams.schemes.environment.prefixes', ['' => [$envPath]]); + } + + /** + * @return $this + * @throws RuntimeException + * @throws InvalidArgumentException + */ + public function init() + { + $locator = new UniformResourceLocator(GRAV_WEBROOT); + $files = []; + + $guard = 5; + do { + $check = $files; + $this->initializeLocator($locator); + $files = $locator->findResources('config://streams.yaml'); + + if ($check === $files) { + break; + } + + // Update streams. + foreach (array_reverse($files) as $path) { + $file = CompiledYamlFile::instance($path); + $content = (array)$file->content(); + if (!empty($content['schemes'])) { + $this->items['streams']['schemes'] = $content['schemes'] + $this->items['streams']['schemes']; + } + } + } while (--$guard); + + if (!$guard) { + throw new RuntimeException('Setup: Configuration reload loop detected!'); + } + + // Make sure we have valid setup. + $this->check($locator); + + return $this; + } + + /** + * Initialize resource locator by using the configuration. + * + * @param UniformResourceLocator $locator + * @return void + * @throws BadMethodCallException + */ + public function initializeLocator(UniformResourceLocator $locator) + { + $locator->reset(); + + $schemes = (array) $this->get('streams.schemes', []); + + foreach ($schemes as $scheme => $config) { + if (isset($config['paths'])) { + $locator->addPath($scheme, '', $config['paths']); + } + + $override = $config['override'] ?? false; + $force = $config['force'] ?? false; + + if (isset($config['prefixes'])) { + foreach ((array)$config['prefixes'] as $prefix => $paths) { + $locator->addPath($scheme, $prefix, $paths, $override, $force); + } + } + } + } + + /** + * Get available streams and their types from the configuration. + * + * @return array + */ + public function getStreams() + { + $schemes = []; + foreach ((array) $this->get('streams.schemes') as $scheme => $config) { + $type = $config['type'] ?? 'ReadOnlyStream'; + if ($type[0] !== '\\') { + $type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type; + } + + $schemes[$scheme] = $type; + } + + return $schemes; + } + + /** + * @param UniformResourceLocator $locator + * @return void + * @throws InvalidArgumentException + * @throws BadMethodCallException + * @throws RuntimeException + */ + protected function check(UniformResourceLocator $locator) + { + $streams = $this->items['streams']['schemes'] ?? null; + if (!is_array($streams)) { + throw new InvalidArgumentException('Configuration is missing streams.schemes!'); + } + $diff = array_keys(array_diff_key($this->streams, $streams)); + if ($diff) { + throw new InvalidArgumentException( + sprintf('Configuration is missing keys %s from streams.schemes!', implode(', ', $diff)) + ); + } + + try { + // If environment is found, remove all missing override locations (B/C compatibility). + if ($locator->findResource('environment://', true)) { + $force = $this->get('streams.schemes.environment.force', false); + if (!$force) { + $prefixes = $this->get('streams.schemes.environment.prefixes.'); + $update = false; + foreach ($prefixes as $i => $prefix) { + if ($locator->isStream($prefix)) { + if ($locator->findResource($prefix, true)) { + break; + } + } elseif (file_exists($prefix)) { + break; + } + + unset($prefixes[$i]); + $update = true; + } + + if ($update) { + $this->set('streams.schemes.environment.prefixes', ['' => array_values($prefixes)]); + $this->initializeLocator($locator); + } + } + } + + if (!$locator->findResource('environment://config', true)) { + // If environment does not have its own directory, remove it from the lookup. + $prefixes = $this->get('streams.schemes.environment.prefixes'); + $prefixes['config'] = []; + + $this->set('streams.schemes.environment.prefixes', $prefixes); + $this->initializeLocator($locator); + } + + // Create security.yaml salt if it doesn't exist into existing configuration environment if possible. + $securityFile = basename(static::$securityFile); + $securityFolder = substr(static::$securityFile, 0, -\strlen($securityFile)); + $securityFolder = $locator->findResource($securityFolder, true) ?: $locator->findResource($securityFolder, true, true); + $filename = "{$securityFolder}/{$securityFile}"; + + $security_file = CompiledYamlFile::instance($filename); + $security_content = (array)$security_file->content(); + + if (!isset($security_content['salt'])) { + $security_content = array_merge($security_content, ['salt' => Utils::generateRandomString(14)]); + $security_file->content($security_content); + $security_file->save(); + $security_file->free(); + } + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e); + } + } +} diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php new file mode 100644 index 0000000..2878199 --- /dev/null +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -0,0 +1,593 @@ +blueprintSchema) { + $this->blueprintSchema = clone $this->blueprintSchema; + } + } + + /** + * @param string $scope + * @return void + */ + public function setScope($scope) + { + $this->scope = $scope; + } + + /** + * @param object $object + * @return void + */ + public function setObject($object) + { + $this->object = $object; + } + + /** + * Set default values for field types. + * + * @param array $types + * @return $this + */ + public function setTypes(array $types) + { + $this->initInternals(); + + $this->blueprintSchema->setTypes($types); + + return $this; + } + + /** + * @param string $name + * @return array|mixed|null + * @since 1.7 + */ + public function getDefaultValue(string $name) + { + $path = explode('.', $name) ?: []; + $current = $this->getDefaults(); + + foreach ($path as $field) { + if (is_object($current) && isset($current->{$field})) { + $current = $current->{$field}; + } elseif (is_array($current) && isset($current[$field])) { + $current = $current[$field]; + } else { + return null; + } + } + + return $current; + } + + /** + * Get nested structure containing default values defined in the blueprints. + * + * Fields without default value are ignored in the list. + * + * @return array + */ + public function getDefaults() + { + $this->initInternals(); + + if (null === $this->defaults) { + $this->defaults = $this->blueprintSchema->getDefaults(); + } + + return $this->defaults; + } + + /** + * Initialize blueprints with its dynamic fields. + * + * @return $this + */ + public function init() + { + foreach ($this->dynamic as $key => $data) { + // Locate field. + $path = explode('/', $key); + $current = &$this->items; + + foreach ($path as $field) { + if (is_object($current)) { + // Handle objects. + if (!isset($current->{$field})) { + $current->{$field} = []; + } + + $current = &$current->{$field}; + } else { + // Handle arrays and scalars. + if (!is_array($current)) { + $current = [$field => []]; + } elseif (!isset($current[$field])) { + $current[$field] = []; + } + + $current = &$current[$field]; + } + } + + // Set dynamic property. + foreach ($data as $property => $call) { + $action = $call['action']; + $method = 'dynamic' . ucfirst($action); + $call['object'] = $this->object; + + if (isset($this->handlers[$action])) { + $callable = $this->handlers[$action]; + $callable($current, $property, $call); + } elseif (method_exists($this, $method)) { + $this->{$method}($current, $property, $call); + } + } + } + + return $this; + } + + /** + * Extend blueprint with another blueprint. + * + * @param BlueprintForm|array $extends + * @param bool $append + * @return $this + */ + public function extend($extends, $append = false) + { + parent::extend($extends, $append); + + $this->deepInit($this->items); + + return $this; + } + + /** + * @param string $name + * @param mixed $value + * @param string $separator + * @param bool $append + * @return $this + */ + public function embed($name, $value, $separator = '/', $append = false) + { + parent::embed($name, $value, $separator, $append); + + $this->deepInit($this->items); + + return $this; + } + + /** + * Merge two arrays by using blueprints. + * + * @param array $data1 + * @param array $data2 + * @param string|null $name Optional + * @param string $separator Optional + * @return array + */ + public function mergeData(array $data1, array $data2, $name = null, $separator = '.') + { + $this->initInternals(); + + return $this->blueprintSchema->mergeData($data1, $data2, $name, $separator); + } + + /** + * Process data coming from a form. + * + * @param array $data + * @param array $toggles + * @return array + */ + public function processForm(array $data, array $toggles = []) + { + $this->initInternals(); + + return $this->blueprintSchema->processForm($data, $toggles); + } + + /** + * Return data fields that do not exist in blueprints. + * + * @param array $data + * @param string $prefix + * @return array + */ + public function extra(array $data, $prefix = '') + { + $this->initInternals(); + + return $this->blueprintSchema->extra($data, $prefix); + } + + /** + * Validate data against blueprints. + * + * @param array $data + * @param array $options + * @return void + * @throws RuntimeException + */ + public function validate(array $data, array $options = []) + { + $this->initInternals(); + + $this->blueprintSchema->validate($data, $options); + } + + /** + * Filter data by using blueprints. + * + * @param array $data + * @param bool $missingValuesAsNull + * @param bool $keepEmptyValues + * @return array + */ + public function filter(array $data, bool $missingValuesAsNull = false, bool $keepEmptyValues = false) + { + $this->initInternals(); + + return $this->blueprintSchema->filter($data, $missingValuesAsNull, $keepEmptyValues) ?? []; + } + + + /** + * Flatten data by using blueprints. + * + * @param array $data + * @param bool $includeAll + * @return array + */ + public function flattenData(array $data, bool $includeAll = false) + { + $this->initInternals(); + + return $this->blueprintSchema->flattenData($data, $includeAll); + } + + + /** + * Return blueprint data schema. + * + * @return BlueprintSchema + */ + public function schema() + { + $this->initInternals(); + + return $this->blueprintSchema; + } + + /** + * @param string $name + * @param callable $callable + * @return void + */ + public function addDynamicHandler(string $name, callable $callable): void + { + $this->handlers[$name] = $callable; + } + + /** + * Initialize validator. + * + * @return void + */ + protected function initInternals() + { + if (null === $this->blueprintSchema) { + $types = Grav::instance()['plugins']->formFieldTypes; + + $this->blueprintSchema = new BlueprintSchema; + + if ($types) { + $this->blueprintSchema->setTypes($types); + } + + $this->blueprintSchema->embed('', $this->items); + $this->blueprintSchema->init(); + $this->defaults = null; + } + } + + /** + * @param string $filename + * @return array + */ + protected function loadFile($filename) + { + $file = CompiledYamlFile::instance($filename); + $content = (array)$file->content(); + $file->free(); + + return $content; + } + + /** + * @param string|array $path + * @param string|null $context + * @return array + */ + protected function getFiles($path, $context = null) + { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + if (is_string($path) && !$locator->isStream($path)) { + if (is_file($path)) { + return [$path]; + } + + // Find path overrides. + if (null === $context) { + $paths = (array) ($this->overrides[$path] ?? null); + } else { + $paths = []; + } + + // Add path pointing to default context. + if ($context === null) { + $context = $this->context; + } + + if ($context && $context[strlen($context)-1] !== '/') { + $context .= '/'; + } + + $path = $context . $path; + + if (!preg_match('/\.yaml$/', $path)) { + $path .= '.yaml'; + } + + $paths[] = $path; + } else { + $paths = (array) $path; + } + + $files = []; + foreach ($paths as $lookup) { + if (is_string($lookup) && strpos($lookup, '://')) { + $files = array_merge($files, $locator->findResources($lookup)); + } else { + $files[] = $lookup; + } + } + + return array_values(array_unique($files)); + } + + /** + * @param array $field + * @param string $property + * @param array $call + * @return void + */ + protected function dynamicData(array &$field, $property, array &$call) + { + $params = $call['params']; + + if (is_array($params)) { + $function = array_shift($params); + } else { + $function = $params; + $params = []; + } + + [$o, $f] = explode('::', $function, 2); + + $data = null; + if (!$f) { + if (function_exists($o)) { + $data = call_user_func_array($o, $params); + } + } else { + if (method_exists($o, $f)) { + $data = call_user_func_array([$o, $f], $params); + } + } + + // If function returns a value, + if (null !== $data) { + if (is_array($data) && isset($field[$property]) && is_array($field[$property])) { + // Combine field and @data-field together. + $field[$property] += $data; + } else { + // Or create/replace field with @data-field. + $field[$property] = $data; + } + } + } + + /** + * @param array $field + * @param string $property + * @param array $call + * @return void + */ + protected function dynamicConfig(array &$field, $property, array &$call) + { + $params = $call['params']; + if (is_array($params)) { + $value = array_shift($params); + $params = array_shift($params); + } else { + $value = $params; + $params = []; + } + + $default = $field[$property] ?? null; + $config = Grav::instance()['config']->get($value, $default); + if (!empty($field['value_only'])) { + $config = array_combine($config, $config); + } + + if (null !== $config) { + if (!empty($params['append']) && is_array($config) && isset($field[$property]) && is_array($field[$property])) { + // Combine field and @config-field together. + $field[$property] += $config; + } else { + // Or create/replace field with @config-field. + $field[$property] = $config; + } + } + } + + /** + * @param array $field + * @param string $property + * @param array $call + * @return void + */ + protected function dynamicSecurity(array &$field, $property, array &$call) + { + if ($property || !empty($field['validate']['ignore'])) { + return; + } + + $grav = Grav::instance(); + $actions = (array)$call['params']; + + /** @var UserInterface|null $user */ + $user = $grav['user'] ?? null; + $success = null !== $user; + if ($success) { + $success = $this->resolveActions($user, $actions); + } + if (!$success) { + $this->addPropertyRecursive($field, 'validate', ['ignore' => true]); + } + } + + /** + * @param UserInterface|null $user + * @param array $actions + * @param string $op + * @return bool + */ + protected function resolveActions(?UserInterface $user, array $actions, string $op = 'and') + { + if (null === $user) { + return false; + } + + $c = $i = count($actions); + foreach ($actions as $key => $action) { + if (!is_int($key) && is_array($actions)) { + $i -= $this->resolveActions($user, $action, $key); + } elseif ($user->authorize($action)) { + $i--; + } + } + + if ($op === 'and') { + return $i === 0; + } + + return $c !== $i; + } + + /** + * @param array $field + * @param string $property + * @param array $call + * @return void + */ + protected function dynamicScope(array &$field, $property, array &$call) + { + if ($property && $property !== 'ignore') { + return; + } + + $scopes = (array)$call['params']; + $matches = in_array($this->scope, $scopes, true); + if ($this->scope && $property !== 'ignore') { + $matches = !$matches; + } + + if ($matches) { + $this->addPropertyRecursive($field, 'validate', ['ignore' => true]); + return; + } + } + + /** + * @param array $field + * @param string $property + * @param mixed $value + * @return void + */ + protected function addPropertyRecursive(array &$field, $property, $value) + { + if (is_array($value) && isset($field[$property]) && is_array($field[$property])) { + $field[$property] = array_merge_recursive($field[$property], $value); + } else { + $field[$property] = $value; + } + + if (!empty($field['fields'])) { + foreach ($field['fields'] as $key => &$child) { + $this->addPropertyRecursive($child, $property, $value); + } + } + } +} diff --git a/system/src/Grav/Common/Data/BlueprintSchema.php b/system/src/Grav/Common/Data/BlueprintSchema.php new file mode 100644 index 0000000..db5e6af --- /dev/null +++ b/system/src/Grav/Common/Data/BlueprintSchema.php @@ -0,0 +1,422 @@ + true, 'xss_check' => true]; + + /** @var array */ + protected $ignoreFormKeys = [ + 'title' => true, + 'help' => true, + 'placeholder' => true, + 'placeholder_key' => true, + 'placeholder_value' => true, + 'fields' => true + ]; + + /** + * @return array + */ + public function getTypes() + { + return $this->types; + } + + /** + * @param string $name + * @return array + */ + public function getType($name) + { + return $this->types[$name] ?? []; + } + + /** + * @param string $name + * @return array|null + */ + public function getNestedRules(string $name) + { + return $this->getNested($name); + } + + /** + * Validate data against blueprints. + * + * @param array $data + * @param array $options + * @return void + * @throws RuntimeException + */ + public function validate(array $data, array $options = []) + { + try { + $validation = $this->items['']['form']['validation'] ?? 'loose'; + $messages = $this->validateArray($data, $this->nested, $validation === 'strict', $options['xss_check'] ?? true); + } catch (RuntimeException $e) { + throw (new ValidationException($e->getMessage(), $e->getCode(), $e))->setMessages(); + } + + if (!empty($messages)) { + throw (new ValidationException('', 400))->setMessages($messages); + } + } + + /** + * @param array $data + * @param array $toggles + * @return array + */ + public function processForm(array $data, array $toggles = []) + { + return $this->processFormRecursive($data, $toggles, $this->nested) ?? []; + } + + /** + * Filter data by using blueprints. + * + * @param array $data Incoming data, for example from a form. + * @param bool $missingValuesAsNull Include missing values as nulls. + * @param bool $keepEmptyValues Include empty values. + * @return array + */ + public function filter(array $data, $missingValuesAsNull = false, $keepEmptyValues = false) + { + $this->buildIgnoreNested($this->nested); + + return $this->filterArray($data, $this->nested, '', $missingValuesAsNull, $keepEmptyValues) ?? []; + } + + /** + * Flatten data by using blueprints. + * + * @param array $data Data to be flattened. + * @param bool $includeAll + * @return array + */ + public function flattenData(array $data, bool $includeAll = false) + { + $list = []; + if ($includeAll) { + foreach ($this->items as $key => $rules) { + $type = $rules['type'] ?? ''; + if (!str_starts_with($type, '_') && !str_contains($key, '*')) { + $list[$key] = null; + } + } + } + + return array_replace($list, $this->flattenArray($data, $this->nested, '')); + } + + /** + * @param array $data + * @param array $rules + * @param string $prefix + * @return array + */ + protected function flattenArray(array $data, array $rules, string $prefix) + { + $array = []; + + foreach ($data as $key => $field) { + $val = $rules[$key] ?? $rules['*'] ?? null; + $rule = is_string($val) ? $this->items[$val] : null; + + if ($rule || isset($val['*'])) { + // Item has been defined in blueprints. + $array[$prefix.$key] = $field; + } elseif (is_array($field) && is_array($val)) { + // Array has been defined in blueprints. + $array += $this->flattenArray($field, $val, $prefix . $key . '.'); + } else { + // Undefined/extra item. + $array[$prefix.$key] = $field; + } + } + + return $array; + } + + /** + * @param array $data + * @param array $rules + * @param bool $strict + * @param bool $xss + * @return array + * @throws RuntimeException + */ + protected function validateArray(array $data, array $rules, bool $strict, bool $xss = true) + { + $messages = $this->checkRequired($data, $rules); + + foreach ($data as $key => $child) { + $val = $rules[$key] ?? $rules['*'] ?? null; + $rule = is_string($val) ? $this->items[$val] : null; + $checkXss = $xss; + + if ($rule) { + // Item has been defined in blueprints. + if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) { + // Skip validation in the ignored field. + continue; + } + + $messages += Validation::validate($child, $rule); + + } elseif (is_array($child) && is_array($val)) { + // Array has been defined in blueprints. + $messages += $this->validateArray($child, $val, $strict); + $checkXss = false; + + } elseif ($strict) { + // Undefined/extra item in strict mode. + /** @var Config $config */ + $config = Grav::instance()['config']; + if (!$config->get('system.strict_mode.blueprint_strict_compat', true)) { + throw new RuntimeException(sprintf('%s is not defined in blueprints', $key), 400); + } + + user_error(sprintf('Having extra key %s in your data is deprecated with blueprint having \'validation: strict\'', $key), E_USER_DEPRECATED); + } + + if ($checkXss) { + $messages += Validation::checkSafety($child, $rule ?: ['name' => $key]); + } + } + + return $messages; + } + + /** + * @param array $data + * @param array $rules + * @param string $parent + * @param bool $missingValuesAsNull + * @param bool $keepEmptyValues + * @return array|null + */ + protected function filterArray(array $data, array $rules, string $parent, bool $missingValuesAsNull, bool $keepEmptyValues) + { + $results = []; + + foreach ($data as $key => $field) { + $val = $rules[$key] ?? $rules['*'] ?? null; + $rule = is_string($val) ? $this->items[$val] : $this->items[$parent . $key] ?? null; + + if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) { + // Skip any data in the ignored field. + unset($results[$key]); + continue; + } + + if (null === $field) { + if ($missingValuesAsNull) { + $results[$key] = null; + } else { + unset($results[$key]); + } + continue; + } + + $isParent = isset($val['*']); + $type = $rule['type'] ?? null; + + if (!$isParent && $type && $type !== '_parent') { + $field = Validation::filter($field, $rule); + } elseif (is_array($field) && is_array($val)) { + // Array has been defined in blueprints. + $k = $isParent ? '*' : $key; + $field = $this->filterArray($field, $val, $parent . $k . '.', $missingValuesAsNull, $keepEmptyValues); + + if (null === $field) { + // Nested parent has no values. + unset($results[$key]); + continue; + } + } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') { + // Skip any extra data. + continue; + } + + if ($keepEmptyValues || (null !== $field && (!is_array($field) || !empty($field)))) { + $results[$key] = $field; + } + } + + return $results ?: null; + } + + /** + * @param array $nested + * @param string $parent + * @return bool + */ + protected function buildIgnoreNested(array $nested, $parent = '') + { + $ignore = true; + foreach ($nested as $key => $val) { + $key = $parent . $key; + if (is_array($val)) { + $ignore = $this->buildIgnoreNested($val, $key . '.') && $ignore; // Keep the order! + } else { + $child = $this->items[$key] ?? null; + $ignore = $ignore && (!$child || !empty($child['disabled']) || !empty($child['validate']['ignore'])); + } + } + if ($ignore) { + $key = trim($parent, '.'); + $this->items[$key]['validate']['ignore'] = true; + } + + return $ignore; + } + + /** + * @param array|null $data + * @param array $toggles + * @param array $nested + * @return array|null + */ + protected function processFormRecursive(?array $data, array $toggles, array $nested) + { + foreach ($nested as $key => $value) { + if ($key === '') { + continue; + } + if ($key === '*') { + // TODO: Add support to collections. + continue; + } + if (is_array($value)) { + // Special toggle handling for all the nested data. + $toggle = $toggles[$key] ?? []; + if (!is_array($toggle)) { + if (!$toggle) { + $data[$key] = null; + + continue; + } + + $toggle = []; + } + // Recursively fetch the items. + $childData = $data[$key] ?? null; + if (null !== $childData && !is_array($childData)) { + throw new \RuntimeException(sprintf("Bad form data for field collection '%s': %s used instead of an array", $key, gettype($childData))); + } + $data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggle, $value); + } else { + $field = $this->get($value); + // Do not add the field if: + if ( + // Not an input field + !$field + // Field has been disabled + || !empty($field['disabled']) + // Field validation is set to be ignored + || !empty($field['validate']['ignore']) + // Field is overridable and the toggle is turned off + || (!empty($field['overridable']) && empty($toggles[$key])) + ) { + continue; + } + if (!isset($data[$key])) { + $data[$key] = null; + } + } + } + + return $data; + } + + /** + * @param array $data + * @param array $fields + * @return array + */ + protected function checkRequired(array $data, array $fields) + { + $messages = []; + + foreach ($fields as $name => $field) { + if (!is_string($field)) { + continue; + } + + $field = $this->items[$field]; + + // Skip ignored field, it will not be required. + if (!empty($field['disabled']) || !empty($field['validate']['ignore'])) { + continue; + } + + // Skip overridable fields without value. + // TODO: We need better overridable support, which is not just ignoring required values but also looking if defaults are good. + if (!empty($field['overridable']) && !isset($data[$name])) { + continue; + } + + // Check if required. + if (isset($field['validate']['required']) + && $field['validate']['required'] === true) { + if (isset($data[$name])) { + continue; + } + if ($field['type'] === 'file' && isset($data['data']['name'][$name])) { //handle case of file input fields required + continue; + } + + $value = $field['label'] ?? $field['name']; + $language = Grav::instance()['language']; + $message = sprintf($language->translate('GRAV.FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $language->translate($value)); + $messages[$field['name']][] = $message; + } + } + + return $messages; + } + + /** + * @param array $field + * @param string $property + * @param array $call + * @return void + */ + protected function dynamicConfig(array &$field, $property, array &$call) + { + $value = $call['params']; + + $default = $field[$property] ?? null; + $config = Grav::instance()['config']->get($value, $default); + + if (null !== $config) { + $field[$property] = $config; + } + } +} diff --git a/system/src/Grav/Common/Data/Blueprints.php b/system/src/Grav/Common/Data/Blueprints.php new file mode 100644 index 0000000..abe153f --- /dev/null +++ b/system/src/Grav/Common/Data/Blueprints.php @@ -0,0 +1,121 @@ +search = $search; + } + + /** + * Get blueprint. + * + * @param string $type Blueprint type. + * @return Blueprint + * @throws RuntimeException + */ + public function get($type) + { + if (!isset($this->instances[$type])) { + $blueprint = $this->loadFile($type); + $this->instances[$type] = $blueprint; + } + + return $this->instances[$type]; + } + + /** + * Get all available blueprint types. + * + * @return array List of type=>name + */ + public function types() + { + if ($this->types === null) { + $this->types = []; + + $grav = Grav::instance(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + // Get stream / directory iterator. + if ($locator->isStream($this->search)) { + $iterator = $locator->getIterator($this->search); + } else { + $iterator = new DirectoryIterator($this->search); + } + + foreach ($iterator as $file) { + if (!$file->isFile() || '.' . $file->getExtension() !== YAML_EXT) { + continue; + } + $name = $file->getBasename(YAML_EXT); + $this->types[$name] = ucfirst(str_replace('_', ' ', $name)); + } + } + + return $this->types; + } + + + /** + * Load blueprint file. + * + * @param string $name Name of the blueprint. + * @return Blueprint + */ + protected function loadFile($name) + { + $blueprint = new Blueprint($name); + + if (is_array($this->search) || is_object($this->search)) { + // Page types. + $blueprint->setOverrides($this->search); + $blueprint->setContext('blueprints://pages'); + } else { + $blueprint->setContext($this->search); + } + + try { + $blueprint->load()->init(); + } catch (RuntimeException $e) { + $log = Grav::instance()['log']; + $log->error(sprintf('Blueprint %s cannot be loaded: %s', $name, $e->getMessage())); + + throw $e; + } + + return $blueprint; + } +} diff --git a/system/src/Grav/Common/Data/Data.php b/system/src/Grav/Common/Data/Data.php new file mode 100644 index 0000000..4de437d --- /dev/null +++ b/system/src/Grav/Common/Data/Data.php @@ -0,0 +1,342 @@ +items = $items; + if (null !== $blueprints) { + $this->blueprints = $blueprints; + } + } + + /** + * @param bool $value + * @return $this + */ + public function setKeepEmptyValues(bool $value) + { + $this->keepEmptyValues = $value; + + return $this; + } + + /** + * @param bool $value + * @return $this + */ + public function setMissingValuesAsNull(bool $value) + { + $this->missingValuesAsNull = $value; + + return $this; + } + + /** + * Get value by using dot notation for nested arrays/objects. + * + * @example $value = $data->value('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function value($name, $default = null, $separator = '.') + { + return $this->get($name, $default, $separator); + } + + /** + * Join nested values together by using blueprints. + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return $this + * @throws RuntimeException + */ + public function join($name, $value, $separator = '.') + { + $old = $this->get($name, null, $separator); + if ($old !== null) { + if (!is_array($old)) { + throw new RuntimeException('Value ' . $old); + } + + if (is_object($value)) { + $value = (array) $value; + } elseif (!is_array($value)) { + throw new RuntimeException('Value ' . $value); + } + + $value = $this->blueprints()->mergeData($old, $value, $name, $separator); + } + + $this->set($name, $value, $separator); + + return $this; + } + + /** + * Get nested structure containing default values defined in the blueprints. + * + * Fields without default value are ignored in the list. + + * @return array + */ + public function getDefaults() + { + return $this->blueprints()->getDefaults(); + } + + /** + * Set default values by using blueprints. + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return $this + */ + public function joinDefaults($name, $value, $separator = '.') + { + if (is_object($value)) { + $value = (array) $value; + } + + $old = $this->get($name, null, $separator); + if ($old !== null) { + $value = $this->blueprints()->mergeData($value, $old, $name, $separator); + } + + $this->set($name, $value, $separator); + + return $this; + } + + /** + * Get value from the configuration and join it with given data. + * + * @param string $name Dot separated path to the requested value. + * @param array|object $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return array + * @throws RuntimeException + */ + public function getJoined($name, $value, $separator = '.') + { + if (is_object($value)) { + $value = (array) $value; + } elseif (!is_array($value)) { + throw new RuntimeException('Value ' . $value); + } + + $old = $this->get($name, null, $separator); + + if ($old === null) { + // No value set; no need to join data. + return $value; + } + + if (!is_array($old)) { + throw new RuntimeException('Value ' . $old); + } + + // Return joined data. + return $this->blueprints()->mergeData($old, $value, $name, $separator); + } + + + /** + * Merge two configurations together. + * + * @param array $data + * @return $this + */ + public function merge(array $data) + { + $this->items = $this->blueprints()->mergeData($this->items, $data); + + return $this; + } + + /** + * Set default values to the configuration if variables were not set. + * + * @param array $data + * @return $this + */ + public function setDefaults(array $data) + { + $this->items = $this->blueprints()->mergeData($data, $this->items); + + return $this; + } + + /** + * Validate by blueprints. + * + * @return $this + * @throws Exception + */ + public function validate() + { + $this->blueprints()->validate($this->items); + + return $this; + } + + /** + * @return $this + */ + public function filter() + { + $args = func_get_args(); + $missingValuesAsNull = (bool)(array_shift($args) ?? $this->missingValuesAsNull); + $keepEmptyValues = (bool)(array_shift($args) ?? $this->keepEmptyValues); + + $this->items = $this->blueprints()->filter($this->items, $missingValuesAsNull, $keepEmptyValues); + + return $this; + } + + /** + * Get extra items which haven't been defined in blueprints. + * + * @return array + */ + public function extra() + { + return $this->blueprints()->extra($this->items); + } + + /** + * Return blueprints. + * + * @return Blueprint + */ + public function blueprints() + { + if (null === $this->blueprints) { + $this->blueprints = new Blueprint(); + } elseif (is_callable($this->blueprints)) { + // Lazy load blueprints. + $blueprints = $this->blueprints; + $this->blueprints = $blueprints(); + } + + return $this->blueprints; + } + + /** + * Save data if storage has been defined. + * + * @return void + * @throws RuntimeException + */ + public function save() + { + $file = $this->file(); + if ($file) { + $file->save($this->items); + } + } + + /** + * Returns whether the data already exists in the storage. + * + * NOTE: This method does not check if the data is current. + * + * @return bool + */ + public function exists() + { + $file = $this->file(); + + return $file && $file->exists(); + } + + /** + * Return unmodified data as raw string. + * + * NOTE: This function only returns data which has been saved to the storage. + * + * @return string + */ + public function raw() + { + $file = $this->file(); + + return $file ? $file->raw() : ''; + } + + /** + * Set or get the data storage. + * + * @param FileInterface|null $storage Optionally enter a new storage. + * @return FileInterface|null + */ + public function file(FileInterface $storage = null) + { + if ($storage) { + $this->storage = $storage; + } + + return $this->storage; + } + + /** + * @return array + */ + public function jsonSerialize() + { + return $this->items; + } +} diff --git a/system/src/Grav/Common/Data/DataInterface.php b/system/src/Grav/Common/Data/DataInterface.php new file mode 100644 index 0000000..b4452bb --- /dev/null +++ b/system/src/Grav/Common/Data/DataInterface.php @@ -0,0 +1,84 @@ +value('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function value($name, $default = null, $separator = '.'); + + /** + * Merge external data. + * + * @param array $data + * @return mixed + */ + public function merge(array $data); + + /** + * Return blueprints. + * + * @return Blueprint + */ + public function blueprints(); + + /** + * Validate by blueprints. + * + * @return $this + * @throws Exception + */ + public function validate(); + + /** + * Filter all items by using blueprints. + * + * @return $this + */ + public function filter(); + + /** + * Get extra items which haven't been defined in blueprints. + * + * @return array + */ + public function extra(); + + /** + * Save data into the file. + * + * @return void + */ + public function save(); + + /** + * Set or get the data storage. + * + * @param FileInterface|null $storage Optionally enter a new storage. + * @return FileInterface + */ + public function file(FileInterface $storage = null); +} diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php new file mode 100644 index 0000000..bfb4057 --- /dev/null +++ b/system/src/Grav/Common/Data/Validation.php @@ -0,0 +1,1214 @@ +translate($field['validate']['message']) + : $language->translate('GRAV.FORM.INVALID_INPUT') . ' "' . $language->translate($name) . '"'; + + + // Validate type with fallback type text. + $method = 'type' . str_replace('-', '_', $type); + + // If this is a YAML field validate/filter as such + if (isset($field['yaml']) && $field['yaml'] === true) { + $method = 'typeYaml'; + } + + $messages = []; + + $success = method_exists(__CLASS__, $method) ? self::$method($value, $validate, $field) : true; + if (!$success) { + $messages[$field['name']][] = $message; + } + + // Check individual rules. + foreach ($validate as $rule => $params) { + $method = 'validate' . ucfirst(str_replace('-', '_', $rule)); + + if (method_exists(__CLASS__, $method)) { + $success = self::$method($value, $params); + + if (!$success) { + $messages[$field['name']][] = $message; + } + } + } + + return $messages; + } + + /** + * @param mixed $value + * @param array $field + * @return array + */ + public static function checkSafety($value, array $field) + { + $messages = []; + + $type = $field['validate']['type'] ?? $field['type'] ?? 'text'; + $options = $field['xss_check'] ?? []; + if ($options === false || $type === 'unset') { + return $messages; + } + if (!is_array($options)) { + $options = []; + } + + $name = ucfirst($field['label'] ?? $field['name'] ?? 'UNKNOWN'); + + /** @var UserInterface $user */ + $user = Grav::instance()['user'] ?? null; + /** @var Config $config */ + $config = Grav::instance()['config']; + + $xss_whitelist = $config->get('security.xss_whitelist', 'admin.super'); + + // Get language class. + /** @var Language $language */ + $language = Grav::instance()['language']; + + if (!static::authorize($xss_whitelist, $user)) { + $defaults = Security::getXssDefaults(); + $options += $defaults; + $options['enabled_rules'] += $defaults['enabled_rules']; + if (!empty($options['safe_protocols'])) { + $options['invalid_protocols'] = array_diff($options['invalid_protocols'], $options['safe_protocols']); + } + if (!empty($options['safe_tags'])) { + $options['dangerous_tags'] = array_diff($options['dangerous_tags'], $options['safe_tags']); + } + + if (is_string($value)) { + $violation = Security::detectXss($value, $options); + if ($violation) { + $messages[$name][] = $language->translate(['GRAV.FORM.XSS_ISSUES', $language->translate($name)], null, true); + } + } elseif (is_array($value)) { + $violations = Security::detectXssFromArray($value, "{$name}.", $options); + if ($violations) { + $messages[$name][] = $language->translate(['GRAV.FORM.XSS_ISSUES', $language->translate($name)], null, true); + } + } + } + + return $messages; + } + + /** + * Checks user authorisation to the action. + * + * @param string|string[] $action + * @param UserInterface|null $user + * @return bool + */ + public static function authorize($action, UserInterface $user = null) + { + if (!$user) { + return false; + } + + $action = (array)$action; + foreach ($action as $a) { + // Ignore 'admin.super' if it's not the only value to be checked. + if ($a === 'admin.super' && count($action) > 1 && $user instanceof FlexObjectInterface) { + continue; + } + + if ($user->authorize($a)) { + return true; + } + } + + return false; + } + + /** + * Filter value against a blueprint field definition. + * + * @param mixed $value + * @param array $field + * @return mixed Filtered value. + */ + public static function filter($value, array $field) + { + $validate = (array)($field['filter'] ?? $field['validate'] ?? null); + + // If value isn't required, we will return null if empty value is given. + if (($value === null || $value === '') && empty($validate['required'])) { + return null; + } + + if (!isset($field['type'])) { + $field['type'] = 'text'; + } + $type = $field['filter']['type'] ?? $field['validate']['type'] ?? $field['type']; + + $method = 'filter' . ucfirst(str_replace('-', '_', $type)); + + // If this is a YAML field validate/filter as such + if (isset($field['yaml']) && $field['yaml'] === true) { + $method = 'filterYaml'; + } + + if (!method_exists(__CLASS__, $method)) { + $method = isset($field['array']) && $field['array'] === true ? 'filterArray' : 'filterText'; + } + + return self::$method($value, $validate, $field); + } + + /** + * HTML5 input: text + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeText($value, array $params, array $field) + { + if (!is_string($value) && !is_numeric($value)) { + return false; + } + + $value = (string)$value; + + if (!empty($params['trim'])) { + $value = trim($value); + } + + $value = preg_replace("/\r\n|\r/um", "\n", $value); + $len = mb_strlen($value); + + $min = (int)($params['min'] ?? 0); + if ($min && $len < $min) { + return false; + } + + $max = (int)($params['max'] ?? 0); + if ($max && $len > $max) { + return false; + } + + $step = (int)($params['step'] ?? 0); + if ($step && ($len - $min) % $step === 0) { + return false; + } + + if ((!isset($params['multiline']) || !$params['multiline']) && preg_match('/\R/um', $value)) { + return false; + } + + return true; + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return string + */ + protected static function filterText($value, array $params, array $field) + { + if (!is_string($value) && !is_numeric($value)) { + return ''; + } + + $value = (string)$value; + + if (!empty($params['trim'])) { + $value = trim($value); + } + + return preg_replace("/\r\n|\r/um", "\n", $value); + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return string|null + */ + protected static function filterCheckbox($value, array $params, array $field) + { + $value = (string)$value; + $field_value = (string)($field['value'] ?? '1'); + + return $value === $field_value ? $value : null; + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return array|array[]|false|string[] + */ + protected static function filterCommaList($value, array $params, array $field) + { + return is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY); + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return bool + */ + public static function typeCommaList($value, array $params, array $field) + { + return is_array($value) ? true : self::typeText($value, $params, $field); + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return array|array[]|false|string[] + */ + protected static function filterLines($value, array $params, array $field) + { + return is_array($value) ? $value : preg_split('/\s*[\r\n]+\s*/', $value, -1, PREG_SPLIT_NO_EMPTY); + } + + /** + * @param mixed $value + * @param array $params + * @return string + */ + protected static function filterLower($value, array $params) + { + return mb_strtolower($value); + } + + /** + * @param mixed $value + * @param array $params + * @return string + */ + protected static function filterUpper($value, array $params) + { + return mb_strtoupper($value); + } + + + /** + * HTML5 input: textarea + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeTextarea($value, array $params, array $field) + { + if (!isset($params['multiline'])) { + $params['multiline'] = true; + } + + return self::typeText($value, $params, $field); + } + + /** + * HTML5 input: password + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typePassword($value, array $params, array $field) + { + return self::typeText($value, $params, $field); + } + + /** + * HTML5 input: hidden + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeHidden($value, array $params, array $field) + { + return self::typeText($value, $params, $field); + } + + /** + * Custom input: checkbox list + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeCheckboxes($value, array $params, array $field) + { + // Set multiple: true so checkboxes can easily use min/max counts to control number of options required + $field['multiple'] = true; + + return self::typeArray((array) $value, $params, $field); + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return array|null + */ + protected static function filterCheckboxes($value, array $params, array $field) + { + return self::filterArray($value, $params, $field); + } + + /** + * HTML5 input: checkbox + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeCheckbox($value, array $params, array $field) + { + $value = (string)$value; + $field_value = (string)($field['value'] ?? '1'); + + return $value === $field_value; + } + + /** + * HTML5 input: radio + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeRadio($value, array $params, array $field) + { + return self::typeArray((array) $value, $params, $field); + } + + /** + * Custom input: toggle + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeToggle($value, array $params, array $field) + { + if (is_bool($value)) { + $value = (int)$value; + } + + return self::typeArray((array) $value, $params, $field); + } + + /** + * Custom input: file + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeFile($value, array $params, array $field) + { + return self::typeArray((array)$value, $params, $field); + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return array + */ + protected static function filterFile($value, array $params, array $field) + { + return (array)$value; + } + + /** + * HTML5 input: select + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeSelect($value, array $params, array $field) + { + return self::typeArray((array) $value, $params, $field); + } + + /** + * HTML5 input: number + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeNumber($value, array $params, array $field) + { + if (!is_numeric($value)) { + return false; + } + + $value = (float)$value; + + $min = 0; + if (isset($params['min'])) { + $min = (float)$params['min']; + if ($value < $min) { + return false; + } + } + + if (isset($params['max'])) { + $max = (float)$params['max']; + if ($value > $max) { + return false; + } + } + + if (isset($params['step'])) { + $step = (float)$params['step']; + // Count of how many steps we are above/below the minimum value. + $pos = ($value - $min) / $step; + + return is_int(static::filterNumber($pos, $params, $field)); + } + + return true; + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return float|int + */ + protected static function filterNumber($value, array $params, array $field) + { + return (string)(int)$value !== (string)(float)$value ? (float)$value : (int)$value; + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return string + */ + protected static function filterDateTime($value, array $params, array $field) + { + $format = Grav::instance()['config']->get('system.pages.dateformat.default'); + if ($format) { + $converted = new DateTime($value); + return $converted->format($format); + } + return $value; + } + + /** + * HTML5 input: range + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeRange($value, array $params, array $field) + { + return self::typeNumber($value, $params, $field); + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return float|int + */ + protected static function filterRange($value, array $params, array $field) + { + return self::filterNumber($value, $params, $field); + } + + /** + * HTML5 input: color + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeColor($value, array $params, array $field) + { + return (bool)preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value); + } + + /** + * HTML5 input: email + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeEmail($value, array $params, array $field) + { + $values = !is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value; + + foreach ($values as $val) { + if (!(self::typeText($val, $params, $field) && filter_var($val, FILTER_VALIDATE_EMAIL))) { + return false; + } + } + + return true; + } + + /** + * HTML5 input: url + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeUrl($value, array $params, array $field) + { + return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL); + } + + /** + * HTML5 input: datetime + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeDatetime($value, array $params, array $field) + { + if ($value instanceof DateTime) { + return true; + } + if (!is_string($value)) { + return false; + } + if (!isset($params['format'])) { + return false !== strtotime($value); + } + + $dateFromFormat = DateTime::createFromFormat($params['format'], $value); + + return $dateFromFormat && $value === date($params['format'], $dateFromFormat->getTimestamp()); + } + + /** + * HTML5 input: datetime-local + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeDatetimeLocal($value, array $params, array $field) + { + return self::typeDatetime($value, $params, $field); + } + + /** + * HTML5 input: date + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeDate($value, array $params, array $field) + { + if (!isset($params['format'])) { + $params['format'] = 'Y-m-d'; + } + + return self::typeDatetime($value, $params, $field); + } + + /** + * HTML5 input: time + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeTime($value, array $params, array $field) + { + if (!isset($params['format'])) { + $params['format'] = 'H:i'; + } + + return self::typeDatetime($value, $params, $field); + } + + /** + * HTML5 input: month + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeMonth($value, array $params, array $field) + { + if (!isset($params['format'])) { + $params['format'] = 'Y-m'; + } + + return self::typeDatetime($value, $params, $field); + } + + /** + * HTML5 input: week + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeWeek($value, array $params, array $field) + { + if (!isset($params['format']) && !preg_match('/^\d{4}-W\d{2}$/u', $value)) { + return false; + } + + return self::typeDatetime($value, $params, $field); + } + + /** + * Custom input: array + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeArray($value, array $params, array $field) + { + if (!is_array($value)) { + return false; + } + + if (isset($field['multiple'])) { + if (isset($params['min']) && count($value) < $params['min']) { + return false; + } + + if (isset($params['max']) && count($value) > $params['max']) { + return false; + } + + $min = $params['min'] ?? 0; + if (isset($params['step']) && (count($value) - $min) % $params['step'] === 0) { + return false; + } + } + + // If creating new values is allowed, no further checks are needed. + $validateOptions = $field['validate']['options'] ?? null; + if (!empty($field['selectize']['create']) || $validateOptions === 'ignore') { + return true; + } + + $options = $field['options'] ?? []; + $use = $field['use'] ?? 'values'; + + if ($validateOptions) { + // Use custom options structure. + foreach ($options as &$option) { + $option = $option[$validateOptions] ?? null; + } + unset($option); + $options = array_values($options); + } elseif (empty($field['selectize']) || empty($field['multiple'])) { + $options = array_keys($options); + } + if ($use === 'keys') { + $value = array_keys($value); + } + + return !($options && array_diff($value, $options)); + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return array|null + */ + protected static function filterFlatten_array($value, $params, $field) + { + $value = static::filterArray($value, $params, $field); + + return Utils::arrayUnflattenDotNotation($value); + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return array|null + */ + protected static function filterArray($value, $params, $field) + { + $values = (array) $value; + $options = isset($field['options']) ? array_keys($field['options']) : []; + $multi = $field['multiple'] ?? false; + + if (count($values) === 1 && isset($values[0]) && $values[0] === '') { + return null; + } + + + if ($options) { + $useKey = isset($field['use']) && $field['use'] === 'keys'; + foreach ($values as $key => $val) { + $values[$key] = $useKey ? (bool) $val : $val; + } + } + + if ($multi) { + foreach ($values as $key => $val) { + if (is_array($val)) { + $val = implode(',', $val); + $values[$key] = array_map('trim', explode(',', $val)); + } else { + $values[$key] = trim($val); + } + } + } + + $ignoreEmpty = isset($field['ignore_empty']) && Utils::isPositive($field['ignore_empty']); + $valueType = $params['value_type'] ?? null; + $keyType = $params['key_type'] ?? null; + if ($ignoreEmpty || $valueType || $keyType) { + $values = static::arrayFilterRecurse($values, ['value_type' => $valueType, 'key_type' => $keyType, 'ignore_empty' => $ignoreEmpty]); + } + + return $values; + } + + /** + * @param array $values + * @param array $params + * @return array + */ + protected static function arrayFilterRecurse(array $values, array $params): array + { + foreach ($values as $key => &$val) { + if ($params['key_type']) { + switch ($params['key_type']) { + case 'int': + $result = is_int($key); + break; + case 'string': + $result = is_string($key); + break; + default: + $result = false; + } + if (!$result) { + unset($values[$key]); + } + } + if (is_array($val)) { + $val = static::arrayFilterRecurse($val, $params); + if ($params['ignore_empty'] && empty($val)) { + unset($values[$key]); + } + } else { + if ($params['value_type'] && $val !== '' && $val !== null) { + switch ($params['value_type']) { + case 'bool': + if (Utils::isPositive($val)) { + $val = true; + } elseif (Utils::isNegative($val)) { + $val = false; + } else { + // Ignore invalid bool values. + $val = null; + } + break; + case 'int': + $val = (int)$val; + break; + case 'float': + $val = (float)$val; + break; + case 'string': + $val = (string)$val; + break; + case 'trim': + $val = trim($val); + break; + } + } + + if ($params['ignore_empty'] && ($val === '' || $val === null)) { + unset($values[$key]); + } + } + } + + return $values; + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return bool + */ + public static function typeList($value, array $params, array $field) + { + if (!is_array($value)) { + return false; + } + + if (isset($field['fields'])) { + foreach ($value as $key => $item) { + foreach ($field['fields'] as $subKey => $subField) { + $subKey = trim($subKey, '.'); + $subValue = $item[$subKey] ?? null; + self::validate($subValue, $subField); + } + } + } + + return true; + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return array + */ + protected static function filterList($value, array $params, array $field) + { + return (array) $value; + } + + /** + * @param mixed $value + * @param array $params + * @return array + */ + public static function filterYaml($value, $params) + { + if (!is_string($value)) { + return $value; + } + + return (array) Yaml::parse($value); + } + + /** + * Custom input: ignore (will not validate) + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeIgnore($value, array $params, array $field) + { + return true; + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return mixed + */ + public static function filterIgnore($value, array $params, array $field) + { + return $value; + } + + /** + * Input value which can be ignored. + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeUnset($value, array $params, array $field) + { + return true; + } + + /** + * @param mixed $value + * @param array $params + * @param array $field + * @return null + */ + public static function filterUnset($value, array $params, array $field) + { + return null; + } + + // HTML5 attributes (min, max and range are handled inside the types) + + /** + * @param mixed $value + * @param bool $params + * @return bool + */ + public static function validateRequired($value, $params) + { + if (is_scalar($value)) { + return (bool) $params !== true || $value !== ''; + } + + return (bool) $params !== true || !empty($value); + } + + /** + * @param mixed $value + * @param string $params + * @return bool + */ + public static function validatePattern($value, $params) + { + return (bool) preg_match("`^{$params}$`u", $value); + } + + // Internal types + + /** + * @param mixed $value + * @param mixed $params + * @return bool + */ + public static function validateAlpha($value, $params) + { + return ctype_alpha($value); + } + + /** + * @param mixed $value + * @param mixed $params + * @return bool + */ + public static function validateAlnum($value, $params) + { + return ctype_alnum($value); + } + + /** + * @param mixed $value + * @param mixed $params + * @return bool + */ + public static function typeBool($value, $params) + { + return is_bool($value) || $value == 1 || $value == 0; + } + + /** + * @param mixed $value + * @param mixed $params + * @return bool + */ + public static function validateBool($value, $params) + { + return is_bool($value) || $value == 1 || $value == 0; + } + + /** + * @param mixed $value + * @param mixed $params + * @return bool + */ + protected static function filterBool($value, $params) + { + return (bool) $value; + } + + /** + * @param mixed $value + * @param mixed $params + * @return bool + */ + public static function validateDigit($value, $params) + { + return ctype_digit($value); + } + + /** + * @param mixed $value + * @param mixed $params + * @return bool + */ + public static function validateFloat($value, $params) + { + return is_float(filter_var($value, FILTER_VALIDATE_FLOAT)); + } + + /** + * @param mixed $value + * @param mixed $params + * @return float + */ + protected static function filterFloat($value, $params) + { + return (float) $value; + } + + /** + * @param mixed $value + * @param mixed $params + * @return bool + */ + public static function validateHex($value, $params) + { + return ctype_xdigit($value); + } + + /** + * Custom input: int + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeInt($value, array $params, array $field) + { + $params['step'] = max(1, (int)($params['step'] ?? 0)); + + return self::typeNumber($value, $params, $field); + } + + /** + * @param mixed $value + * @param mixed $params + * @return bool + */ + public static function validateInt($value, $params) + { + return is_numeric($value) && (int)$value == $value; + } + + /** + * @param mixed $value + * @param mixed $params + * @return int + */ + protected static function filterInt($value, $params) + { + return (int)$value; + } + + /** + * @param mixed $value + * @param mixed $params + * @return bool + */ + public static function validateArray($value, $params) + { + return is_array($value) || ($value instanceof ArrayAccess && $value instanceof Traversable && $value instanceof Countable); + } + + /** + * @param mixed $value + * @param mixed $params + * @return array + */ + public static function filterItem_List($value, $params) + { + return array_values(array_filter($value, static function ($v) { + return !empty($v); + })); + } + + /** + * @param mixed $value + * @param mixed $params + * @return bool + */ + public static function validateJson($value, $params) + { + return (bool) (@json_decode($value)); + } +} diff --git a/system/src/Grav/Common/Data/ValidationException.php b/system/src/Grav/Common/Data/ValidationException.php new file mode 100644 index 0000000..be6a674 --- /dev/null +++ b/system/src/Grav/Common/Data/ValidationException.php @@ -0,0 +1,67 @@ +messages = $messages; + + $language = Grav::instance()['language']; + $this->message = $language->translate('GRAV.FORM.VALIDATION_FAIL', null, true) . ' ' . $this->message; + + foreach ($messages as $list) { + $list = array_unique($list); + foreach ($list as $message) { + $this->message .= '
' . htmlspecialchars($message, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } + } + + return $this; + } + + public function setSimpleMessage(bool $escape = true): void + { + $first = reset($this->messages); + $message = reset($first); + + $this->message = $escape ? htmlspecialchars($message, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $message; + } + + /** + * @return array + */ + public function getMessages(): array + { + return $this->messages; + } + + public function jsonSerialize(): array + { + return ['validation' => $this->messages]; + } +} diff --git a/system/src/Grav/Common/Debugger.php b/system/src/Grav/Common/Debugger.php new file mode 100644 index 0000000..b8143f6 --- /dev/null +++ b/system/src/Grav/Common/Debugger.php @@ -0,0 +1,1140 @@ +currentTime = microtime(true); + + if (!defined('GRAV_REQUEST_TIME')) { + define('GRAV_REQUEST_TIME', $this->currentTime); + } + + $this->requestTime = $_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME; + + // Set deprecation collector. + $this->setErrorHandler(); + } + + /** + * @return Clockwork|null + */ + public function getClockwork(): ?Clockwork + { + return $this->enabled ? $this->clockwork : null; + } + + /** + * Initialize the debugger + * + * @return $this + * @throws DebugBarException + */ + public function init() + { + if ($this->initialized) { + return $this; + } + + $this->grav = Grav::instance(); + $this->config = $this->grav['config']; + + // Enable/disable debugger based on configuration. + $this->enabled = (bool)$this->config->get('system.debugger.enabled'); + $this->censored = (bool)$this->config->get('system.debugger.censored', false); + + if ($this->enabled) { + $this->initialized = true; + + $clockwork = $debugbar = null; + + switch ($this->config->get('system.debugger.provider', 'debugbar')) { + case 'clockwork': + $this->clockwork = $clockwork = new Clockwork(); + break; + default: + $this->debugbar = $debugbar = new DebugBar(); + } + + $plugins_config = (array)$this->config->get('plugins'); + ksort($plugins_config); + + if ($clockwork) { + $log = $this->grav['log']; + $clockwork->setStorage(new FileStorage('cache://clockwork')); + if (extension_loaded('xdebug')) { + $clockwork->addDataSource(new XdebugDataSource()); + } + if ($log instanceof Logger) { + $clockwork->addDataSource(new MonologDataSource($log)); + } + + $timeline = $clockwork->timeline(); + if ($this->requestTime !== GRAV_REQUEST_TIME) { + $event = $timeline->event('Server'); + $event->finalize($this->requestTime, GRAV_REQUEST_TIME); + } + if ($this->currentTime !== GRAV_REQUEST_TIME) { + $event = $timeline->event('Loading'); + $event->finalize(GRAV_REQUEST_TIME, $this->currentTime); + } + $event = $timeline->event('Site Setup'); + $event->finalize($this->currentTime, microtime(true)); + } + + if ($this->censored) { + $censored = ['CENSORED' => true]; + } + + if ($debugbar) { + $debugbar->addCollector(new PhpInfoCollector()); + $debugbar->addCollector(new MessagesCollector()); + if (!$this->censored) { + $debugbar->addCollector(new RequestDataCollector()); + } + $debugbar->addCollector(new TimeDataCollector($this->requestTime)); + $debugbar->addCollector(new MemoryCollector()); + $debugbar->addCollector(new ExceptionsCollector()); + $debugbar->addCollector(new ConfigCollector($censored ?? (array)$this->config->get('system'), 'Config')); + $debugbar->addCollector(new ConfigCollector($censored ?? $plugins_config, 'Plugins')); + $debugbar->addCollector(new ConfigCollector($this->config->get('streams.schemes'), 'Streams')); + + if ($this->requestTime !== GRAV_REQUEST_TIME) { + $debugbar['time']->addMeasure('Server', $debugbar['time']->getRequestStartTime(), GRAV_REQUEST_TIME); + } + if ($this->currentTime !== GRAV_REQUEST_TIME) { + $debugbar['time']->addMeasure('Loading', GRAV_REQUEST_TIME, $this->currentTime); + } + $debugbar['time']->addMeasure('Site Setup', $this->currentTime, microtime(true)); + } + + $this->addMessage('Grav v' . GRAV_VERSION . ' - PHP ' . PHP_VERSION); + $this->config->debug(); + + if ($clockwork) { + $clockwork->info('System Configuration', $censored ?? $this->config->get('system')); + $clockwork->info('Plugins Configuration', $censored ?? $plugins_config); + $clockwork->info('Streams', $this->config->get('streams.schemes')); + } + } + + return $this; + } + + public function finalize(): void + { + if ($this->clockwork && $this->enabled) { + $this->stopProfiling('Profiler Analysis'); + $this->addMeasures(); + + $deprecations = $this->getDeprecations(); + $count = count($deprecations); + if (!$count) { + return; + } + + /** @var UserData $userData */ + $userData = $this->clockwork->userData('Deprecated'); + $userData->counters([ + 'Deprecated' => count($deprecations) + ]); + /* + foreach ($deprecations as &$deprecation) { + $d = $deprecation; + unset($d['message']); + $this->clockwork->log('deprecated', $deprecation['message'], $d); + } + unset($deprecation); + */ + + $userData->table('Your site is using following deprecated features', $deprecations); + } + } + + public function logRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + if (!$this->enabled || !$this->clockwork) { + return $response; + } + + $clockwork = $this->clockwork; + + $this->finalize(); + + $clockwork->timeline()->finalize($request->getAttribute('request_time')); + + if ($this->censored) { + $censored = 'CENSORED'; + $request = $request + ->withCookieParams([$censored => '']) + ->withUploadedFiles([]) + ->withHeader('cookie', $censored); + $request = $request->withParsedBody([$censored => '']); + } + + $clockwork->addDataSource(new PsrMessageDataSource($request, $response)); + + $clockwork->resolveRequest(); + $clockwork->storeRequest(); + + $clockworkRequest = $clockwork->getRequest(); + + $response = $response + ->withHeader('X-Clockwork-Id', $clockworkRequest->id) + ->withHeader('X-Clockwork-Version', $clockwork::VERSION); + + $grav = Grav::instance(); + $basePath = $this->grav['base_url_relative'] . $grav['pages']->base(); + if ($basePath) { + $response = $response->withHeader('X-Clockwork-Path', $basePath . '/__clockwork/'); + } + + return $response->withHeader('Server-Timing', ServerTiming::fromRequest($clockworkRequest)->value()); + } + + + public function debuggerRequest(RequestInterface $request): Response + { + $clockwork = $this->clockwork; + + $headers = [ + 'Content-Type' => 'application/json', + 'Grav-Internal-SkipShutdown' => 1 + ]; + + $path = $request->getUri()->getPath(); + $clockworkDataUri = '#/__clockwork(?:/(?[0-9-]+))?(?:/(?(?:previous|next)))?(?:/(?\d+))?#'; + if (preg_match($clockworkDataUri, $path, $matches) === false) { + $response = ['message' => 'Bad Input']; + + return new Response(400, $headers, json_encode($response)); + } + + $id = $matches['id'] ?? null; + $direction = $matches['direction'] ?? null; + $count = $matches['count'] ?? null; + + $storage = $clockwork->getStorage(); + + if ($direction === 'previous') { + $data = $storage->previous($id, $count); + } elseif ($direction === 'next') { + $data = $storage->next($id, $count); + } elseif ($id === 'latest') { + $data = $storage->latest(); + } else { + $data = $storage->find($id); + } + + if (preg_match('#(?[0-9-]+|latest)/extended#', $path)) { + $clockwork->extendRequest($data); + } + + if (!$data) { + $response = ['message' => 'Not Found']; + + return new Response(404, $headers, json_encode($response)); + } + + $data = is_array($data) ? array_map(static function ($item) { + return $item->toArray(); + }, $data) : $data->toArray(); + + return new Response(200, $headers, json_encode($data)); + } + + /** + * @return void + */ + protected function addMeasures(): void + { + if (!$this->enabled) { + return; + } + + $nowTime = microtime(true); + $clkTimeLine = $this->clockwork ? $this->clockwork->timeline() : null; + $debTimeLine = $this->debugbar ? $this->debugbar['time'] : null; + foreach ($this->timers as $name => $data) { + $description = $data[0]; + $startTime = $data[1] ?? null; + $endTime = $data[2] ?? $nowTime; + if ($clkTimeLine) { + $event = $clkTimeLine->event($description); + $event->finalize($startTime, $endTime); + } elseif ($debTimeLine) { + if ($endTime - $startTime < 0.001) { + continue; + } + + $debTimeLine->addMeasure($description ?? $name, $startTime, $endTime); + } + } + $this->timers = []; + } + + /** + * Set/get the enabled state of the debugger + * + * @param bool|null $state If null, the method returns the enabled value. If set, the method sets the enabled state + * @return bool + */ + public function enabled($state = null) + { + if ($state !== null) { + $this->enabled = (bool)$state; + } + + return $this->enabled; + } + + /** + * Add the debugger assets to the Grav Assets + * + * @return $this + */ + public function addAssets() + { + if ($this->enabled) { + // Only add assets if Page is HTML + $page = $this->grav['page']; + if ($page->templateFormat() !== 'html') { + return $this; + } + + /** @var Assets $assets */ + $assets = $this->grav['assets']; + + // Clockwork specific assets + if ($this->clockwork) { + $assets->addCss('/system/assets/debugger/clockwork.css', ['loading' => 'inline']); + $assets->addJs('/system/assets/debugger/clockwork.js', ['loading' => 'inline']); + } + + + // Debugbar specific assets + if ($this->debugbar) { + // Add jquery library + $assets->add('jquery', 101); + + $this->renderer = $this->debugbar->getJavascriptRenderer(); + $this->renderer->setIncludeVendors(false); + + [$css_files, $js_files] = $this->renderer->getAssets(null, JavascriptRenderer::RELATIVE_URL); + + foreach ((array)$css_files as $css) { + $assets->addCss($css); + } + + $assets->addCss('/system/assets/debugger/phpdebugbar.css', ['loading' => 'inline']); + + foreach ((array)$js_files as $js) { + $assets->addJs($js); + } + } + } + + return $this; + } + + /** + * @param int $limit + * @return array + */ + public function getCaller($limit = 2) + { + $trace = debug_backtrace(false, $limit); + + return array_pop($trace); + } + + /** + * Adds a data collector + * + * @param DataCollectorInterface $collector + * @return $this + * @throws DebugBarException + */ + public function addCollector($collector) + { + if ($this->debugbar && !$this->debugbar->hasCollector($collector->getName())) { + $this->debugbar->addCollector($collector); + } + + return $this; + } + + /** + * Returns a data collector + * + * @param string $name + * @return DataCollectorInterface|null + * @throws DebugBarException + */ + public function getCollector($name) + { + if ($this->debugbar && $this->debugbar->hasCollector($name)) { + return $this->debugbar->getCollector($name); + } + + return null; + } + + /** + * Displays the debug bar + * + * @return $this + */ + public function render() + { + if ($this->enabled && $this->debugbar) { + // Only add assets if Page is HTML + $page = $this->grav['page']; + if (!$this->renderer || $page->templateFormat() !== 'html') { + return $this; + } + + $this->addMeasures(); + $this->addDeprecations(); + + echo $this->renderer->render(); + } + + return $this; + } + + /** + * Sends the data through the HTTP headers + * + * @return $this + */ + public function sendDataInHeaders() + { + if ($this->enabled && $this->debugbar) { + $this->addMeasures(); + $this->addDeprecations(); + $this->debugbar->sendDataInHeaders(); + } + + return $this; + } + + /** + * Returns collected debugger data. + * + * @return array|null + */ + public function getData() + { + if (!$this->enabled || !$this->debugbar) { + return null; + } + + $this->addMeasures(); + $this->addDeprecations(); + $this->timers = []; + + return $this->debugbar->getData(); + } + + /** + * Hierarchical Profiler support. + * + * @param callable $callable + * @param string|null $message + * @return mixed + */ + public function profile(callable $callable, string $message = null) + { + $this->startProfiling(); + $response = $callable(); + $this->stopProfiling($message); + + return $response; + } + + public function addTwigProfiler(Environment $twig): void + { + $clockwork = $this->getClockwork(); + if ($clockwork) { + $source = new TwigClockworkDataSource($twig); + $source->listenToEvents(); + $clockwork->addDataSource($source); + } + } + + /** + * Start profiling code. + * + * @return void + */ + public function startProfiling(): void + { + if ($this->enabled && extension_loaded('tideways_xhprof')) { + $this->profiling++; + if ($this->profiling === 1) { + // @phpstan-ignore-next-line + \tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_NO_BUILTINS); + } + } + } + + /** + * Stop profiling code. Returns profiling array or null if profiling couldn't be done. + * + * @param string|null $message + * @return array|null + */ + public function stopProfiling(string $message = null): ?array + { + $timings = null; + if ($this->enabled && extension_loaded('tideways_xhprof')) { + $profiling = $this->profiling - 1; + if ($profiling === 0) { + // @phpstan-ignore-next-line + $timings = \tideways_xhprof_disable(); + $timings = $this->buildProfilerTimings($timings); + + if ($this->clockwork) { + /** @var UserData $userData */ + $userData = $this->clockwork->userData('Profiler'); + $userData->counters([ + 'Calls' => count($timings) + ]); + $userData->table('Profiler', $timings); + } else { + $this->addMessage($message ?? 'Profiler Analysis', 'debug', $timings); + } + } + $this->profiling = max(0, $profiling); + } + + return $timings; + } + + /** + * @param array $timings + * @return array + */ + protected function buildProfilerTimings(array $timings): array + { + // Filter method calls which take almost no time. + $timings = array_filter($timings, function ($value) { + return $value['wt'] > 50; + }); + + uasort($timings, function (array $a, array $b) { + return $b['wt'] <=> $a['wt']; + }); + + $table = []; + foreach ($timings as $key => $timing) { + $parts = explode('==>', $key); + $method = $this->parseProfilerCall(array_pop($parts)); + $context = $this->parseProfilerCall(array_pop($parts)); + + // Skip redundant method calls. + if ($context === 'Grav\Framework\RequestHandler\RequestHandler::handle()') { + continue; + } + + // Do not profile library calls. + if (strpos($context, 'Grav\\') !== 0) { + continue; + } + + $table[] = [ + 'Context' => $context, + 'Method' => $method, + 'Calls' => $timing['ct'], + 'Time (ms)' => $timing['wt'] / 1000, + ]; + } + + return $table; + } + + /** + * @param string|null $call + * @return mixed|string|null + */ + protected function parseProfilerCall(?string $call) + { + if (null === $call) { + return ''; + } + if (strpos($call, '@')) { + [$call,] = explode('@', $call); + } + if (strpos($call, '::')) { + [$class, $call] = explode('::', $call); + } + + if (!isset($class)) { + return $call; + } + + // It is also possible to display twig files, but they are being logged in views. + /* + if (strpos($class, '__TwigTemplate_') === 0 && class_exists($class)) { + $env = new Environment(); + / ** @var Template $template * / + $template = new $class($env); + + return $template->getTemplateName(); + } + */ + + return "{$class}::{$call}()"; + } + + /** + * Start a timer with an associated name and description + * + * @param string $name + * @param string|null $description + * @return $this + */ + public function startTimer($name, $description = null) + { + $this->timers[$name] = [$description, microtime(true)]; + + return $this; + } + + /** + * Stop the named timer + * + * @param string $name + * @return $this + */ + public function stopTimer($name) + { + if (isset($this->timers[$name])) { + $endTime = microtime(true); + $this->timers[$name][] = $endTime; + } + + return $this; + } + + /** + * Dump variables into the Messages tab of the Debug Bar + * + * @param mixed $message + * @param string $label + * @param mixed|bool $isString + * @return $this + */ + public function addMessage($message, $label = 'info', $isString = true) + { + if ($this->enabled) { + if ($this->censored) { + if (!is_scalar($message)) { + $message = 'CENSORED'; + } + if (!is_scalar($isString)) { + $isString = ['CENSORED']; + } + } + + if ($this->debugbar) { + if (is_array($isString)) { + $message = $isString; + $isString = false; + } elseif (is_string($isString)) { + $message = $isString; + $isString = true; + } + $this->debugbar['messages']->addMessage($message, $label, $isString); + } + + if ($this->clockwork) { + $context = $isString; + if (!is_scalar($message)) { + $context = $message; + $message = gettype($context); + } + if (is_bool($context)) { + $context = []; + } elseif (!is_array($context)) { + $type = gettype($context); + $context = [$type => $context]; + } + + $this->clockwork->log($label, $message, $context); + } + } + + return $this; + } + + /** + * @param string $name + * @param object $event + * @param EventDispatcherInterface $dispatcher + * @param float|null $time + * @return $this + */ + public function addEvent(string $name, $event, EventDispatcherInterface $dispatcher, float $time = null) + { + if ($this->enabled && $this->clockwork) { + $time = $time ?? microtime(true); + $duration = (microtime(true) - $time) * 1000; + + $data = null; + if ($event && method_exists($event, '__debugInfo')) { + $data = $event; + } + + $listeners = []; + foreach ($dispatcher->getListeners($name) as $listener) { + $listeners[] = $this->resolveCallable($listener); + } + + $this->clockwork->addEvent($name, $data, $time, ['listeners' => $listeners, 'duration' => $duration]); + } + + return $this; + } + + /** + * Dump exception into the Messages tab of the Debug Bar + * + * @param Throwable $e + * @return Debugger + */ + public function addException(Throwable $e) + { + if ($this->initialized && $this->enabled) { + if ($this->debugbar) { + $this->debugbar['exceptions']->addThrowable($e); + } + + if ($this->clockwork) { + /** @var UserData $exceptions */ + $exceptions = $this->clockwork->userData('Exceptions'); + $exceptions->data(['message' => $e->getMessage()]); + + $this->clockwork->alert($e->getMessage(), ['exception' => $e]); + } + } + + return $this; + } + + /** + * @return void + */ + public function setErrorHandler() + { + $this->errorHandler = set_error_handler( + [$this, 'deprecatedErrorHandler'] + ); + } + + /** + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param int $errline + * @return bool + */ + public function deprecatedErrorHandler($errno, $errstr, $errfile, $errline) + { + if ($errno !== E_USER_DEPRECATED && $errno !== E_DEPRECATED) { + if ($this->errorHandler) { + return call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline); + } + + return true; + } + + if (!$this->enabled) { + return true; + } + + // Figure out error scope from the error. + $scope = 'unknown'; + if (stripos($errstr, 'grav') !== false) { + $scope = 'grav'; + } elseif (strpos($errfile, '/twig/') !== false) { + $scope = 'twig'; + } elseif (stripos($errfile, '/yaml/') !== false) { + $scope = 'yaml'; + } elseif (strpos($errfile, '/vendor/') !== false) { + $scope = 'vendor'; + } + + // Clean up backtrace to make it more useful. + $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT); + + // Skip current call. + array_shift($backtrace); + + // Find yaml file where the error happened. + if ($scope === 'yaml') { + foreach ($backtrace as $current) { + if (isset($current['args'])) { + foreach ($current['args'] as $arg) { + if ($arg instanceof SplFileInfo) { + $arg = $arg->getPathname(); + } + if (is_string($arg) && preg_match('/.+\.(yaml|md)$/i', $arg)) { + $errfile = $arg; + $errline = 0; + + break 2; + } + } + } + } + } + + // Filter arguments. + $cut = 0; + $previous = null; + foreach ($backtrace as $i => &$current) { + if (isset($current['args'])) { + $args = []; + foreach ($current['args'] as $arg) { + if (is_string($arg)) { + $arg = "'" . $arg . "'"; + if (mb_strlen($arg) > 100) { + $arg = 'string'; + } + } elseif (is_bool($arg)) { + $arg = $arg ? 'true' : 'false'; + } elseif (is_scalar($arg)) { + $arg = $arg; + } elseif (is_object($arg)) { + $arg = get_class($arg) . ' $object'; + } elseif (is_array($arg)) { + $arg = '$array'; + } else { + $arg = '$object'; + } + + $args[] = $arg; + } + $current['args'] = $args; + } + + $object = $current['object'] ?? null; + unset($current['object']); + + $reflection = null; + if ($object instanceof TemplateWrapper) { + $reflection = new ReflectionObject($object); + $property = $reflection->getProperty('template'); + $property->setAccessible(true); + $object = $property->getValue($object); + } + + if ($object instanceof Template) { + $file = $current['file'] ?? null; + + if (preg_match('`(Template.php|TemplateWrapper.php)$`', $file)) { + $current = null; + continue; + } + + $debugInfo = $object->getDebugInfo(); + + $line = 1; + if (!$reflection) { + foreach ($debugInfo as $codeLine => $templateLine) { + if ($codeLine <= $current['line']) { + $line = $templateLine; + break; + } + } + } + + $src = $object->getSourceContext(); + //$code = preg_split('/\r\n|\r|\n/', $src->getCode()); + //$current['twig']['twig'] = trim($code[$line - 1]); + $current['twig']['file'] = $src->getPath(); + $current['twig']['line'] = $line; + + $prevFile = $previous['file'] ?? null; + if ($prevFile && $file === $prevFile) { + $prevLine = $previous['line']; + + $line = 1; + foreach ($debugInfo as $codeLine => $templateLine) { + if ($codeLine <= $prevLine) { + $line = $templateLine; + break; + } + } + + //$previous['twig']['twig'] = trim($code[$line - 1]); + $previous['twig']['file'] = $src->getPath(); + $previous['twig']['line'] = $line; + } + + $cut = $i; + } elseif ($object instanceof ProcessorInterface) { + $cut = $cut ?: $i; + break; + } + + $previous = &$backtrace[$i]; + } + unset($current); + + if ($cut) { + $backtrace = array_slice($backtrace, 0, $cut + 1); + } + $backtrace = array_values(array_filter($backtrace)); + + // Skip vendor libraries and the method where error was triggered. + foreach ($backtrace as $i => $current) { + if (!isset($current['file'])) { + continue; + } + if (strpos($current['file'], '/vendor/') !== false) { + $cut = $i + 1; + continue; + } + if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) { + $cut = $i + 1; + continue; + } + + break; + } + + if ($cut) { + $backtrace = array_slice($backtrace, $cut); + } + $backtrace = array_values(array_filter($backtrace)); + + $current = reset($backtrace); + + // If the issue happened inside twig file, change the file and line to match that file. + $file = $current['twig']['file'] ?? ''; + if ($file) { + $errfile = $file; + $errline = $current['twig']['line'] ?? 0; + } + + $deprecation = [ + 'scope' => $scope, + 'message' => $errstr, + 'file' => $errfile, + 'line' => $errline, + 'trace' => $backtrace, + 'count' => 1 + ]; + + $this->deprecations[] = $deprecation; + + // Do not pass forward. + return true; + } + + /** + * @return array + */ + protected function getDeprecations(): array + { + if (!$this->deprecations) { + return []; + } + + $list = []; + /** @var array $deprecated */ + foreach ($this->deprecations as $deprecated) { + $list[] = $this->getDepracatedMessage($deprecated)[0]; + } + + return $list; + } + + /** + * @return void + * @throws DebugBarException + */ + protected function addDeprecations() + { + if (!$this->deprecations) { + return; + } + + $collector = new MessagesCollector('deprecated'); + $this->addCollector($collector); + $collector->addMessage('Your site is using following deprecated features:'); + + /** @var array $deprecated */ + foreach ($this->deprecations as $deprecated) { + list($message, $scope) = $this->getDepracatedMessage($deprecated); + + $collector->addMessage($message, $scope); + } + } + + /** + * @param array $deprecated + * @return array + */ + protected function getDepracatedMessage($deprecated) + { + $scope = $deprecated['scope']; + + $trace = []; + if (isset($deprecated['trace'])) { + foreach ($deprecated['trace'] as $current) { + $class = $current['class'] ?? ''; + $type = $current['type'] ?? ''; + $function = $this->getFunction($current); + if (isset($current['file'])) { + $current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']); + } + + unset($current['class'], $current['type'], $current['function'], $current['args']); + + if (isset($current['twig'])) { + $trace[] = $current['twig']; + } else { + $trace[] = ['call' => $class . $type . $function] + $current; + } + } + } + + $array = [ + 'message' => $deprecated['message'], + 'file' => $deprecated['file'], + 'line' => $deprecated['line'], + 'trace' => $trace + ]; + + return [ + array_filter($array), + $scope + ]; + } + + /** + * @param array $trace + * @return string + */ + protected function getFunction($trace) + { + if (!isset($trace['function'])) { + return ''; + } + + return $trace['function'] . '(' . implode(', ', $trace['args'] ?? []) . ')'; + } + + /** + * @param callable $callable + * @return string + */ + protected function resolveCallable(callable $callable) + { + if (is_array($callable)) { + return get_class($callable[0]) . '->' . $callable[1] . '()'; + } + + return 'unknown'; + } +} diff --git a/system/src/Grav/Common/Errors/BareHandler.php b/system/src/Grav/Common/Errors/BareHandler.php new file mode 100644 index 0000000..206b57e --- /dev/null +++ b/system/src/Grav/Common/Errors/BareHandler.php @@ -0,0 +1,33 @@ +getInspector(); + $code = $inspector->getException()->getCode(); + if (($code >= 400) && ($code < 600)) { + $this->getRun()->sendHttpCode($code); + } + + return Handler::QUIT; + } +} diff --git a/system/src/Grav/Common/Errors/Errors.php b/system/src/Grav/Common/Errors/Errors.php new file mode 100644 index 0000000..3dc99c6 --- /dev/null +++ b/system/src/Grav/Common/Errors/Errors.php @@ -0,0 +1,85 @@ +get('system.errors'); + $jsonRequest = $_SERVER && isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] === 'application/json'; + + // Setup Whoops-based error handler + $system = new SystemFacade; + $whoops = new Run($system); + + $verbosity = 1; + + if (isset($config['display'])) { + if (is_int($config['display'])) { + $verbosity = $config['display']; + } else { + $verbosity = $config['display'] ? 1 : 0; + } + } + + switch ($verbosity) { + case 1: + $error_page = new PrettyPageHandler(); + $error_page->setPageTitle('Crikey! There was an error...'); + $error_page->addResourcePath(GRAV_ROOT . '/system/assets'); + $error_page->addCustomCss('whoops.css'); + $whoops->prependHandler($error_page); + break; + case -1: + $whoops->prependHandler(new BareHandler); + break; + default: + $whoops->prependHandler(new SimplePageHandler); + break; + } + + if ($jsonRequest || Misc::isAjaxRequest()) { + $whoops->prependHandler(new JsonResponseHandler()); + } + + if (isset($config['log']) && $config['log']) { + $logger = $grav['log']; + $whoops->pushHandler(function ($exception, $inspector, $run) use ($logger) { + try { + $logger->addCritical($exception->getMessage() . ' - Trace: ' . $exception->getTraceAsString()); + } catch (Exception $e) { + echo $e; + } + }); + } + + $whoops->register(); + + // Re-register deprecation handler. + $grav['debugger']->setErrorHandler(); + } +} diff --git a/system/src/Grav/Common/Errors/Resources/error.css b/system/src/Grav/Common/Errors/Resources/error.css new file mode 100644 index 0000000..11ce3fd --- /dev/null +++ b/system/src/Grav/Common/Errors/Resources/error.css @@ -0,0 +1,52 @@ +html, body { + height: 100% +} +body { + margin:0 3rem; + padding:0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 1.5rem; + line-height: 1.4; + display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ + display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ + display: -ms-flexbox; /* TWEENER - IE 10 */ + display: -webkit-flex; /* NEW - Chrome */ + display: flex; + -webkit-align-items: center; + align-items: center; + -webkit-justify-content: center; + justify-content: center; +} +.container { + margin: 0rem; + max-width: 600px; + padding-bottom:5rem; +} + +header { + color: #000; + font-size: 4rem; + letter-spacing: 2px; + line-height: 1.1; + margin-bottom: 2rem; +} +p { + font-family: Optima, Segoe, "Segoe UI", Candara, Calibri, Arial, sans-serif; + color: #666; +} + +h5 { + font-weight: normal; + color: #999; + font-size: 1rem; +} + +h6 { + font-weight: normal; + color: #999; +} + +code { + font-weight: bold; + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} diff --git a/system/src/Grav/Common/Errors/Resources/layout.html.php b/system/src/Grav/Common/Errors/Resources/layout.html.php new file mode 100644 index 0000000..6699959 --- /dev/null +++ b/system/src/Grav/Common/Errors/Resources/layout.html.php @@ -0,0 +1,30 @@ + + + + + + Whoops there was an error! + + + +
+
+
+ Server Error +
+ + + +

Sorry, something went terribly wrong!

+ +

-

+ +
For further details please review your logs/ folder, or enable displaying of errors in your system configuration.
+
+
+ + diff --git a/system/src/Grav/Common/Errors/SimplePageHandler.php b/system/src/Grav/Common/Errors/SimplePageHandler.php new file mode 100644 index 0000000..0118929 --- /dev/null +++ b/system/src/Grav/Common/Errors/SimplePageHandler.php @@ -0,0 +1,122 @@ +searchPaths[] = __DIR__ . '/Resources'; + } + + /** + * @return int + */ + public function handle() + { + $inspector = $this->getInspector(); + + $helper = new TemplateHelper(); + $templateFile = $this->getResource('layout.html.php'); + $cssFile = $this->getResource('error.css'); + + $code = $inspector->getException()->getCode(); + if (($code >= 400) && ($code < 600)) { + $this->getRun()->sendHttpCode($code); + } + $message = $inspector->getException()->getMessage(); + + if ($inspector->getException() instanceof ErrorException) { + $code = Misc::translateErrorCode($code); + } + + $vars = array( + 'stylesheet' => file_get_contents($cssFile), + 'code' => $code, + 'message' => filter_var(rawurldecode($message), FILTER_SANITIZE_STRING), + ); + + $helper->setVariables($vars); + $helper->render($templateFile); + + return Handler::QUIT; + } + + /** + * @param string $resource + * @return string + * @throws RuntimeException + */ + protected function getResource($resource) + { + // If the resource was found before, we can speed things up + // by caching its absolute, resolved path: + if (isset($this->resourceCache[$resource])) { + return $this->resourceCache[$resource]; + } + + // Search through available search paths, until we find the + // resource we're after: + foreach ($this->searchPaths as $path) { + $fullPath = "{$path}/{$resource}"; + + if (is_file($fullPath)) { + // Cache the result: + $this->resourceCache[$resource] = $fullPath; + return $fullPath; + } + } + + // If we got this far, nothing was found. + throw new RuntimeException( + "Could not find resource '{$resource}' in any resource paths (searched: " . implode(', ', $this->searchPaths). ')' + ); + } + + /** + * @param string $path + * @return void + */ + public function addResourcePath($path) + { + if (!is_dir($path)) { + throw new InvalidArgumentException( + "'{$path}' is not a valid directory" + ); + } + + array_unshift($this->searchPaths, $path); + } + + /** + * @return array + */ + public function getResourcePaths() + { + return $this->searchPaths; + } +} diff --git a/system/src/Grav/Common/Errors/SystemFacade.php b/system/src/Grav/Common/Errors/SystemFacade.php new file mode 100644 index 0000000..8a7f1ce --- /dev/null +++ b/system/src/Grav/Common/Errors/SystemFacade.php @@ -0,0 +1,46 @@ +whoopsShutdownHandler = $function; + register_shutdown_function([$this, 'handleShutdown']); + } + + /** + * Special case to deal with Fatal errors and the like. + * + * @return void + */ + public function handleShutdown() + { + $error = $this->getLastError(); + + // Ignore core warnings and errors. + if ($error && !($error['type'] & (E_CORE_WARNING | E_CORE_ERROR))) { + $handler = $this->whoopsShutdownHandler; + $handler(); + } + } +} diff --git a/system/src/Grav/Common/File/CompiledFile.php b/system/src/Grav/Common/File/CompiledFile.php new file mode 100644 index 0000000..66fa620 --- /dev/null +++ b/system/src/Grav/Common/File/CompiledFile.php @@ -0,0 +1,122 @@ +raw === null && $this->content === null) { + $key = md5($this->filename); + $file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php"); + + $modified = $this->modified(); + if (!$modified) { + try { + return $this->decode($this->raw()); + } catch (Throwable $e) { + // If the compiled file is broken, we can safely ignore the error and continue. + } + } + + $class = get_class($this); + + $cache = $file->exists() ? $file->content() : null; + + // Load real file if cache isn't up to date (or is invalid). + if (!isset($cache['@class']) + || $cache['@class'] !== $class + || $cache['modified'] !== $modified + || $cache['filename'] !== $this->filename + ) { + // Attempt to lock the file for writing. + try { + $file->lock(false); + } catch (Exception $e) { + // Another process has locked the file; we will check this in a bit. + } + + // Decode RAW file into compiled array. + $data = (array)$this->decode($this->raw()); + $cache = [ + '@class' => $class, + 'filename' => $this->filename, + 'modified' => $modified, + 'data' => $data + ]; + + // If compiled file wasn't already locked by another process, save it. + if ($file->locked() !== false) { + $file->save($cache); + $file->unlock(); + + // Compile cached file into bytecode cache + if (function_exists('opcache_invalidate')) { + // Silence error if function exists, but is restricted. + @opcache_invalidate($file->filename(), true); + } + } + } + $file->free(); + + $this->content = $cache['data']; + } + } catch (Exception $e) { + throw new RuntimeException(sprintf('Failed to read %s: %s', basename($this->filename), $e->getMessage()), 500, $e); + } + + return parent::content($var); + } + + /** + * Serialize file. + * + * @return array + */ + public function __sleep() + { + return [ + 'filename', + 'extension', + 'raw', + 'content', + 'settings' + ]; + } + + /** + * Unserialize file. + */ + public function __wakeup() + { + if (!isset(static::$instances[$this->filename])) { + static::$instances[$this->filename] = $this; + } + } +} diff --git a/system/src/Grav/Common/File/CompiledJsonFile.php b/system/src/Grav/Common/File/CompiledJsonFile.php new file mode 100644 index 0000000..dae95ba --- /dev/null +++ b/system/src/Grav/Common/File/CompiledJsonFile.php @@ -0,0 +1,33 @@ + ['.DS_Store'], + 'exclude_paths' => [] + ]; + + /** @var string */ + protected $archive_file; + + /** + * @param string $compression + * @return ZipArchiver + */ + public static function create($compression) + { + if ($compression === 'zip') { + return new ZipArchiver(); + } + + return new ZipArchiver(); + } + + /** + * @param string $archive_file + * @return $this + */ + public function setArchive($archive_file) + { + $this->archive_file = $archive_file; + + return $this; + } + + /** + * @param array $options + * @return $this + */ + public function setOptions($options) + { + // Set infinite PHP execution time if possible. + if (Utils::functionExists('set_time_limit')) { + @set_time_limit(0); + } + + $this->options = $options + $this->options; + + return $this; + } + + /** + * @param string $folder + * @param callable|null $status + * @return $this + */ + abstract public function compress($folder, callable $status = null); + + /** + * @param string $destination + * @param callable|null $status + * @return $this + */ + abstract public function extract($destination, callable $status = null); + + /** + * @param array $folders + * @param callable|null $status + * @return $this + */ + abstract public function addEmptyFolders($folders, callable $status = null); + + /** + * @param string $rootPath + * @return RecursiveIteratorIterator + */ + protected function getArchiveFiles($rootPath) + { + $exclude_paths = $this->options['exclude_paths']; + $exclude_files = $this->options['exclude_files']; + $dirItr = new RecursiveDirectoryIterator($rootPath, RecursiveDirectoryIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS | FilesystemIterator::UNIX_PATHS); + $filterItr = new RecursiveDirectoryFilterIterator($dirItr, $rootPath, $exclude_paths, $exclude_files); + $files = new RecursiveIteratorIterator($filterItr, RecursiveIteratorIterator::SELF_FIRST); + + return $files; + } +} diff --git a/system/src/Grav/Common/Filesystem/Folder.php b/system/src/Grav/Common/Filesystem/Folder.php new file mode 100644 index 0000000..ecbc6ce --- /dev/null +++ b/system/src/Grav/Common/Filesystem/Folder.php @@ -0,0 +1,541 @@ +isStream($path)) { + $directory = $locator->getRecursiveIterator($path, $flags); + } else { + $directory = new RecursiveDirectoryIterator($path, $flags); + } + $filter = new RecursiveFolderFilterIterator($directory); + $iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST); + + foreach ($iterator as $dir) { + $dir_modified = $dir->getMTime(); + if ($dir_modified > $last_modified) { + $last_modified = $dir_modified; + } + } + + return $last_modified; + } + + /** + * Recursively find the last modified time under given path by file. + * + * @param string $path + * @param string $extensions which files to search for specifically + * @return int + */ + public static function lastModifiedFile($path, $extensions = 'md|yaml') + { + if (!file_exists($path)) { + return 0; + } + + $last_modified = 0; + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $flags = RecursiveDirectoryIterator::SKIP_DOTS; + if ($locator->isStream($path)) { + $directory = $locator->getRecursiveIterator($path, $flags); + } else { + $directory = new RecursiveDirectoryIterator($path, $flags); + } + $recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); + $iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/i'); + + /** @var RecursiveDirectoryIterator $file */ + foreach ($iterator as $filepath => $file) { + try { + $file_modified = $file->getMTime(); + if ($file_modified > $last_modified) { + $last_modified = $file_modified; + } + } catch (Exception $e) { + Grav::instance()['log']->error('Could not process file: ' . $e->getMessage()); + } + } + + return $last_modified; + } + + /** + * Recursively md5 hash all files in a path + * + * @param string $path + * @return string + */ + public static function hashAllFiles($path) + { + $files = []; + + if (file_exists($path)) { + $flags = RecursiveDirectoryIterator::SKIP_DOTS; + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + if ($locator->isStream($path)) { + $directory = $locator->getRecursiveIterator($path, $flags); + } else { + $directory = new RecursiveDirectoryIterator($path, $flags); + } + + $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); + + foreach ($iterator as $file) { + $files[] = $file->getPathname() . '?'. $file->getMTime(); + } + } + + return md5(serialize($files)); + } + + /** + * Get relative path between target and base path. If path isn't relative, return full path. + * + * @param string $path + * @param string $base + * @return string + */ + public static function getRelativePath($path, $base = GRAV_ROOT) + { + if ($base) { + $base = preg_replace('![\\\/]+!', '/', $base); + $path = preg_replace('![\\\/]+!', '/', $path); + if (strpos($path, $base) === 0) { + $path = ltrim(substr($path, strlen($base)), '/'); + } + } + + return $path; + } + + /** + * Get relative path between target and base path. If path isn't relative, return full path. + * + * @param string $path + * @param string $base + * @return string + */ + public static function getRelativePathDotDot($path, $base) + { + // Normalize paths. + $base = preg_replace('![\\\/]+!', '/', $base); + $path = preg_replace('![\\\/]+!', '/', $path); + + if ($path === $base) { + return ''; + } + + $baseParts = explode('/', ltrim($base, '/')); + $pathParts = explode('/', ltrim($path, '/')); + + array_pop($baseParts); + $lastPart = array_pop($pathParts); + foreach ($baseParts as $i => $directory) { + if (isset($pathParts[$i]) && $pathParts[$i] === $directory) { + unset($baseParts[$i], $pathParts[$i]); + } else { + break; + } + } + $pathParts[] = $lastPart; + $path = str_repeat('../', count($baseParts)) . implode('/', $pathParts); + + return '' === $path + || strpos($path, '/') === 0 + || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos) + ? "./$path" : $path; + } + + /** + * Shift first directory out of the path. + * + * @param string $path + * @return string|null + */ + public static function shift(&$path) + { + $parts = explode('/', trim($path, '/'), 2); + $result = array_shift($parts); + $path = array_shift($parts); + + return $result ?: null; + } + + /** + * Return recursive list of all files and directories under given path. + * + * @param string $path + * @param array $params + * @return array + * @throws RuntimeException + */ + public static function all($path, array $params = []) + { + if (!$path) { + throw new RuntimeException("Path doesn't exist."); + } + if (!file_exists($path)) { + return []; + } + + $compare = isset($params['compare']) ? 'get' . $params['compare'] : null; + $pattern = $params['pattern'] ?? null; + $filters = $params['filters'] ?? null; + $recursive = $params['recursive'] ?? true; + $levels = $params['levels'] ?? -1; + $key = isset($params['key']) ? 'get' . $params['key'] : null; + $value = 'get' . ($params['value'] ?? ($recursive ? 'SubPathname' : 'Filename')); + $folders = $params['folders'] ?? true; + $files = $params['files'] ?? true; + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + if ($recursive) { + $flags = RecursiveDirectoryIterator::SKIP_DOTS + FilesystemIterator::UNIX_PATHS + + FilesystemIterator::CURRENT_AS_SELF + FilesystemIterator::FOLLOW_SYMLINKS; + if ($locator->isStream($path)) { + $directory = $locator->getRecursiveIterator($path, $flags); + } else { + $directory = new RecursiveDirectoryIterator($path, $flags); + } + $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); + $iterator->setMaxDepth(max($levels, -1)); + } else { + if ($locator->isStream($path)) { + $iterator = $locator->getIterator($path); + } else { + $iterator = new FilesystemIterator($path); + } + } + + $results = []; + + /** @var RecursiveDirectoryIterator $file */ + foreach ($iterator as $file) { + // Ignore hidden files. + if (strpos($file->getFilename(), '.') === 0 && $file->isFile()) { + continue; + } + if (!$folders && $file->isDir()) { + continue; + } + if (!$files && $file->isFile()) { + continue; + } + if ($compare && $pattern && !preg_match($pattern, $file->{$compare}())) { + continue; + } + $fileKey = $key ? $file->{$key}() : null; + $filePath = $file->{$value}(); + if ($filters) { + if (isset($filters['key'])) { + $pre = !empty($filters['pre-key']) ? $filters['pre-key'] : ''; + $fileKey = $pre . preg_replace($filters['key'], '', $fileKey); + } + if (isset($filters['value'])) { + $filter = $filters['value']; + if (is_callable($filter)) { + $filePath = $filter($file); + } else { + $filePath = preg_replace($filter, '', $filePath); + } + } + } + + if ($fileKey !== null) { + $results[$fileKey] = $filePath; + } else { + $results[] = $filePath; + } + } + + return $results; + } + + /** + * Recursively copy directory in filesystem. + * + * @param string $source + * @param string $target + * @param string|null $ignore Ignore files matching pattern (regular expression). + * @return void + * @throws RuntimeException + */ + public static function copy($source, $target, $ignore = null) + { + $source = rtrim($source, '\\/'); + $target = rtrim($target, '\\/'); + + if (!is_dir($source)) { + throw new RuntimeException('Cannot copy non-existing folder.'); + } + + // Make sure that path to the target exists before copying. + self::create($target); + + $success = true; + + // Go through all sub-directories and copy everything. + $files = self::all($source); + foreach ($files as $file) { + if ($ignore && preg_match($ignore, $file)) { + continue; + } + $src = $source .'/'. $file; + $dst = $target .'/'. $file; + + if (is_dir($src)) { + // Create current directory (if it doesn't exist). + if (!is_dir($dst)) { + $success &= @mkdir($dst, 0777, true); + } + } else { + // Or copy current file. + $success &= @copy($src, $dst); + } + } + + if (!$success) { + $error = error_get_last(); + throw new RuntimeException($error['message'] ?? 'Unknown error'); + } + + // Make sure that the change will be detected when caching. + @touch(dirname($target)); + } + + /** + * Move directory in filesystem. + * + * @param string $source + * @param string $target + * @return void + * @throws RuntimeException + */ + public static function move($source, $target) + { + if (!file_exists($source) || !is_dir($source)) { + // Rename fails if source folder does not exist. + throw new RuntimeException('Cannot move non-existing folder.'); + } + + // Don't do anything if the source is the same as the new target + if ($source === $target) { + return; + } + + if (strpos($target, $source . '/') === 0) { + throw new RuntimeException('Cannot move folder to itself'); + } + + if (file_exists($target)) { + // Rename fails if target folder exists. + throw new RuntimeException('Cannot move files to existing folder/file.'); + } + + // Make sure that path to the target exists before moving. + self::create(dirname($target)); + + // Silence warnings (chmod failed etc). + @rename($source, $target); + + // Rename function can fail while still succeeding, so let's check if the folder exists. + if (is_dir($source)) { + // Rename doesn't support moving folders across filesystems. Use copy instead. + self::copy($source, $target); + self::delete($source); + } + + // Make sure that the change will be detected when caching. + @touch(dirname($source)); + @touch(dirname($target)); + @touch($target); + } + + /** + * Recursively delete directory from filesystem. + * + * @param string $target + * @param bool $include_target + * @return bool + * @throws RuntimeException + */ + public static function delete($target, $include_target = true) + { + if (!is_dir($target)) { + return false; + } + + $success = self::doDelete($target, $include_target); + + if (!$success) { + $error = error_get_last(); + + throw new RuntimeException($error['message'] ?? 'Unknown error'); + } + + // Make sure that the change will be detected when caching. + if ($include_target) { + @touch(dirname($target)); + } else { + @touch($target); + } + + return $success; + } + + /** + * @param string $folder + * @return void + * @throws RuntimeException + */ + public static function mkdir($folder) + { + self::create($folder); + } + + /** + * @param string $folder + * @return void + * @throws RuntimeException + */ + public static function create($folder) + { + // Silence error for open_basedir; should fail in mkdir instead. + if (@is_dir($folder)) { + return; + } + + $success = @mkdir($folder, 0777, true); + + if (!$success) { + // Take yet another look, make sure that the folder doesn't exist. + clearstatcache(true, $folder); + if (!@is_dir($folder)) { + throw new RuntimeException(sprintf('Unable to create directory: %s', $folder)); + } + } + } + + /** + * Recursive copy of one directory to another + * + * @param string $src + * @param string $dest + * @return bool + * @throws RuntimeException + */ + public static function rcopy($src, $dest) + { + + // If the src is not a directory do a simple file copy + if (!is_dir($src)) { + copy($src, $dest); + return true; + } + + // If the destination directory does not exist create it + if (!is_dir($dest)) { + static::create($dest); + } + + // Open the source directory to read in files + $i = new DirectoryIterator($src); + foreach ($i as $f) { + if ($f->isFile()) { + copy($f->getRealPath(), "{$dest}/" . $f->getFilename()); + } else { + if (!$f->isDot() && $f->isDir()) { + static::rcopy($f->getRealPath(), "{$dest}/{$f}"); + } + } + } + return true; + } + + /** + * Does a directory contain children + * + * @param string $directory + * @return int|false + */ + public static function countChildren($directory) + { + if (!is_dir($directory)) { + return false; + } + $directories = glob($directory . '/*', GLOB_ONLYDIR); + + return count($directories); + } + + /** + * @param string $folder + * @param bool $include_target + * @return bool + * @internal + */ + protected static function doDelete($folder, $include_target = true) + { + // Special case for symbolic links. + if ($include_target && is_link($folder)) { + return @unlink($folder); + } + + // Go through all items in filesystem and recursively remove everything. + $files = array_diff(scandir($folder, SCANDIR_SORT_NONE), array('.', '..')); + foreach ($files as $file) { + $path = "{$folder}/{$file}"; + is_dir($path) ? self::doDelete($path) : @unlink($path); + } + + return $include_target ? @rmdir($folder) : true; + } +} diff --git a/system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php b/system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php new file mode 100644 index 0000000..0dac81c --- /dev/null +++ b/system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php @@ -0,0 +1,82 @@ +current(); + $filename = $file->getFilename(); + $relative_filename = str_replace($this::$root . '/', '', $file->getPathname()); + + if ($file->isDir()) { + if (in_array($relative_filename, $this::$ignore_folders, true)) { + return false; + } + if (!in_array($filename, $this::$ignore_files, true)) { + return true; + } + } elseif ($file->isFile() && !in_array($filename, $this::$ignore_files, true)) { + return true; + } + return false; + } + + /** + * @return RecursiveDirectoryFilterIterator|RecursiveFilterIterator + */ + public function getChildren() + { + /** @var RecursiveDirectoryFilterIterator $iterator */ + $iterator = $this->getInnerIterator(); + + return new self($iterator->getChildren(), $this::$root, $this::$ignore_folders, $this::$ignore_files); + } +} diff --git a/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php b/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php new file mode 100644 index 0000000..ded0259 --- /dev/null +++ b/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php @@ -0,0 +1,55 @@ +get('system.pages.ignore_folders'); + } + + $this::$ignore_folders = $ignore_folders; + } + + /** + * Check whether the current element of the iterator is acceptable + * + * @return bool true if the current element is acceptable, otherwise false. + */ + public function accept() + { + /** @var SplFileInfo $current */ + $current = $this->current(); + + return $current->isDir() && !in_array($current->getFilename(), $this::$ignore_folders, true); + } +} diff --git a/system/src/Grav/Common/Filesystem/ZipArchiver.php b/system/src/Grav/Common/Filesystem/ZipArchiver.php new file mode 100644 index 0000000..6e53e70 --- /dev/null +++ b/system/src/Grav/Common/Filesystem/ZipArchiver.php @@ -0,0 +1,135 @@ +open($this->archive_file); + + if ($archive === true) { + Folder::create($destination); + + if (!$zip->extractTo($destination)) { + throw new RuntimeException('ZipArchiver: ZIP failed to extract ' . $this->archive_file . ' to ' . $destination); + } + + $zip->close(); + + return $this; + } + + throw new RuntimeException('ZipArchiver: Failed to open ' . $this->archive_file); + } + + /** + * @param string $source + * @param callable|null $status + * @return $this + */ + public function compress($source, callable $status = null) + { + if (!extension_loaded('zip')) { + throw new InvalidArgumentException('ZipArchiver: Zip PHP module not installed...'); + } + + // Get real path for our folder + $rootPath = realpath($source); + if (!$rootPath) { + throw new InvalidArgumentException('ZipArchiver: ' . $source . ' cannot be found...'); + } + + $zip = new ZipArchive(); + if (!$zip->open($this->archive_file, ZipArchive::CREATE)) { + throw new InvalidArgumentException('ZipArchiver:' . $this->archive_file . ' cannot be created...'); + } + + $files = $this->getArchiveFiles($rootPath); + + $status && $status([ + 'type' => 'count', + 'steps' => iterator_count($files), + ]); + + foreach ($files as $file) { + $filePath = $file->getPathname(); + $relativePath = ltrim(substr($filePath, strlen($rootPath)), '/'); + + if ($file->isDir()) { + $zip->addEmptyDir($relativePath); + } else { + $zip->addFile($filePath, $relativePath); + } + + $status && $status([ + 'type' => 'progress', + ]); + } + + $status && $status([ + 'type' => 'message', + 'message' => 'Compressing...' + ]); + + $zip->close(); + + return $this; + } + + /** + * @param array $folders + * @param callable|null $status + * @return $this + */ + public function addEmptyFolders($folders, callable $status = null) + { + if (!extension_loaded('zip')) { + throw new InvalidArgumentException('ZipArchiver: Zip PHP module not installed...'); + } + + $zip = new ZipArchive(); + if (!$zip->open($this->archive_file)) { + throw new InvalidArgumentException('ZipArchiver: ' . $this->archive_file . ' cannot be opened...'); + } + + $status && $status([ + 'type' => 'message', + 'message' => 'Adding empty folders...' + ]); + + foreach ($folders as $folder) { + $zip->addEmptyDir($folder); + $status && $status([ + 'type' => 'progress', + ]); + } + + $zip->close(); + + return $this; + } +} diff --git a/system/src/Grav/Common/Flex/FlexCollection.php b/system/src/Grav/Common/Flex/FlexCollection.php new file mode 100644 index 0000000..6429c9e --- /dev/null +++ b/system/src/Grav/Common/Flex/FlexCollection.php @@ -0,0 +1,28 @@ + + */ +abstract class FlexCollection extends \Grav\Framework\Flex\FlexCollection +{ + use FlexGravTrait; + use FlexCollectionTrait; +} diff --git a/system/src/Grav/Common/Flex/FlexIndex.php b/system/src/Grav/Common/Flex/FlexIndex.php new file mode 100644 index 0000000..44b6fdd --- /dev/null +++ b/system/src/Grav/Common/Flex/FlexIndex.php @@ -0,0 +1,29 @@ + + */ +abstract class FlexIndex extends \Grav\Framework\Flex\FlexIndex +{ + use FlexGravTrait; + use FlexIndexTrait; +} diff --git a/system/src/Grav/Common/Flex/FlexObject.php b/system/src/Grav/Common/Flex/FlexObject.php new file mode 100644 index 0000000..b64aea1 --- /dev/null +++ b/system/src/Grav/Common/Flex/FlexObject.php @@ -0,0 +1,74 @@ +getNestedProperty($name, null, $separator); + + // Handle media order field. + if (null === $value && $name === 'media_order') { + return implode(',', $this->getMediaOrder()); + } + + // Handle media fields. + $settings = $this->getFieldSettings($name); + if (($settings['media_field'] ?? false) === true) { + return $this->parseFileProperty($value, $settings); + } + + return $value ?? $default; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::prepareStorage() + */ + public function prepareStorage(): array + { + // Remove extra content from media fields. + $fields = $this->getMediaFields(); + foreach ($fields as $field) { + $data = $this->getNestedProperty($field); + if (is_array($data)) { + foreach ($data as $name => &$image) { + unset($image['image_url'], $image['thumb_url']); + } + unset($image); + $this->setNestedProperty($field, $data); + } + } + + return parent::prepareStorage(); + } +} diff --git a/system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php b/system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php new file mode 100644 index 0000000..29b640e --- /dev/null +++ b/system/src/Grav/Common/Flex/Traits/FlexCollectionTrait.php @@ -0,0 +1,51 @@ + 'flex', + 'directory' => $this->getFlexDirectory(), + 'collection' => $this + ]); + } + if (strpos($name, 'onFlexCollection') !== 0 && strpos($name, 'on') === 0) { + $name = 'onFlexCollection' . substr($name, 2); + } + + $container = $this->getContainer(); + if ($event instanceof Event) { + $container->fireEvent($name, $event); + } else { + $container->dispatchEvent($event); + } + + return $this; + } +} diff --git a/system/src/Grav/Common/Flex/Traits/FlexCommonTrait.php b/system/src/Grav/Common/Flex/Traits/FlexCommonTrait.php new file mode 100644 index 0000000..1077e00 --- /dev/null +++ b/system/src/Grav/Common/Flex/Traits/FlexCommonTrait.php @@ -0,0 +1,54 @@ +getContainer(); + + /** @var Twig $twig */ + $twig = $container['twig']; + + try { + return $twig->twig()->resolveTemplate($this->getTemplatePaths($layout)); + } catch (LoaderError $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + return $twig->twig()->resolveTemplate(['flex/404.html.twig']); + } + } + + abstract protected function getTemplatePaths(string $layout): array; + abstract protected function getContainer(): Grav; +} diff --git a/system/src/Grav/Common/Flex/Traits/FlexGravTrait.php b/system/src/Grav/Common/Flex/Traits/FlexGravTrait.php new file mode 100644 index 0000000..9d5c9e0 --- /dev/null +++ b/system/src/Grav/Common/Flex/Traits/FlexGravTrait.php @@ -0,0 +1,74 @@ +getContainer(); + + /** @var Flex $flex */ + $flex = $container['flex']; + + return $flex; + } + + /** + * @return UserInterface|null + */ + protected function getActiveUser(): ?UserInterface + { + $container = $this->getContainer(); + + /** @var UserInterface|null $user */ + $user = $container['user'] ?? null; + + return $user; + } + + /** + * @return bool + */ + protected function isAdminSite(): bool + { + $container = $this->getContainer(); + + return isset($container['admin']); + } + + /** + * @return string + */ + protected function getAuthorizeScope(): string + { + return $this->isAdminSite() ? 'admin' : 'site'; + } +} diff --git a/system/src/Grav/Common/Flex/Traits/FlexIndexTrait.php b/system/src/Grav/Common/Flex/Traits/FlexIndexTrait.php new file mode 100644 index 0000000..1d0ee5c --- /dev/null +++ b/system/src/Grav/Common/Flex/Traits/FlexIndexTrait.php @@ -0,0 +1,20 @@ + 'onFlexObjectRender', + 'onBeforeSave' => 'onFlexObjectBeforeSave', + 'onAfterSave' => 'onFlexObjectAfterSave', + 'onBeforeDelete' => 'onFlexObjectBeforeDelete', + 'onAfterDelete' => 'onFlexObjectAfterDelete' + ]; + + if (null === $event) { + $event = new Event([ + 'type' => 'flex', + 'directory' => $this->getFlexDirectory(), + 'object' => $this + ]); + } + + if (isset($events['name'])) { + $name = $events['name']; + } elseif (strpos($name, 'onFlexObject') !== 0 && strpos($name, 'on') === 0) { + $name = 'onFlexObject' . substr($name, 2); + } + + $container = $this->getContainer(); + if ($event instanceof Event) { + $container->fireEvent($name, $event); + } else { + $container->dispatchEvent($event); + } + + return $this; + } +} diff --git a/system/src/Grav/Common/Flex/Types/Generic/GenericCollection.php b/system/src/Grav/Common/Flex/Types/Generic/GenericCollection.php new file mode 100644 index 0000000..6b2fbc1 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Generic/GenericCollection.php @@ -0,0 +1,24 @@ + + */ +class GenericCollection extends FlexCollection +{ +} diff --git a/system/src/Grav/Common/Flex/Types/Generic/GenericIndex.php b/system/src/Grav/Common/Flex/Types/Generic/GenericIndex.php new file mode 100644 index 0000000..81dbf95 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Generic/GenericIndex.php @@ -0,0 +1,24 @@ + + */ +class GenericIndex extends FlexIndex +{ +} diff --git a/system/src/Grav/Common/Flex/Types/Generic/GenericObject.php b/system/src/Grav/Common/Flex/Types/Generic/GenericObject.php new file mode 100644 index 0000000..4a56752 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Generic/GenericObject.php @@ -0,0 +1,22 @@ + + * + * Incompatibilities with Grav\Common\Page\Collection: + * $page = $collection->key() will not work at all + * $clone = clone $collection does not clone objects inside the collection, does it matter? + * $string = (string)$collection returns collection id instead of comma separated list + * $collection->add() incompatible method signature + * $collection->remove() incompatible method signature + * $collection->filter() incompatible method signature (takes closure instead of callable) + * $collection->prev() does not rewind the internal pointer + * AND most methods are immutable; they do not update the current collection, but return updated one + * + * @method static shuffle() + * @method static select(array $keys) + * @method static unselect(array $keys) + * @method static createFrom(array $elements, string $keyField = null) + * @method PageIndex getIndex() + */ +class PageCollection extends FlexPageCollection implements PageCollectionInterface +{ + use FlexGravTrait; + use FlexCollectionTrait; + + /** @var array|null */ + protected $_params; + + /** + * @return array + */ + public static function getCachedMethods(): array + { + return [ + // Collection specific methods + 'getRoot' => false, + 'getParams' => false, + 'setParams' => false, + 'params' => false, + 'addPage' => false, + 'merge' => false, + 'intersect' => false, + 'prev' => false, + 'nth' => false, + 'random' => false, + 'append' => false, + 'batch' => false, + 'order' => false, + + // Collection filtering + 'dateRange' => true, + 'visible' => true, + 'nonVisible' => true, + 'pages' => true, + 'modules' => true, + 'modular' => true, + 'nonModular' => true, + 'published' => true, + 'nonPublished' => true, + 'routable' => true, + 'nonRoutable' => true, + 'ofType' => true, + 'ofOneOfTheseTypes' => true, + 'ofOneOfTheseAccessLevels' => true, + 'withOrdered' => true, + 'withModules' => true, + 'withPages' => true, + 'withTranslation' => true, + 'filterBy' => true, + + 'toExtendedArray' => false, + 'getLevelListing' => false, + ] + parent::getCachedMethods(); + } + + /** + * @return PageObject + */ + public function getRoot() + { + $index = $this->getIndex(); + + return $index->getRoot(); + } + + /** + * Get the collection params + * + * @return array + */ + public function getParams(): array + { + return $this->_params ?? []; + } + + /** + * Set parameters to the Collection + * + * @param array $params + * @return $this + */ + public function setParams(array $params) + { + $this->_params = $this->_params ? array_merge($this->_params, $params) : $params; + + return $this; + } + + /** + * Get the collection params + * + * @return array + */ + public function params(): array + { + return $this->getParams(); + } + + /** + * Add a single page to a collection + * + * @param PageInterface $page + * @return static + */ + public function addPage(PageInterface $page) + { + if (!$page instanceof PageObject) { + throw new InvalidArgumentException('$page is not a flex page.'); + } + + // FIXME: support other keys. + $this->set($page->getKey(), $page); + + return $this; + } + + /** + * + * Merge another collection with the current collection + * + * @param PageCollectionInterface $collection + * @return static + */ + public function merge(PageCollectionInterface $collection) + { + throw new RuntimeException(__METHOD__ . '(): Not Implemented'); + } + + /** + * Intersect another collection with the current collection + * + * @param PageCollectionInterface $collection + * @return static + */ + public function intersect(PageCollectionInterface $collection) + { + throw new RuntimeException(__METHOD__ . '(): Not Implemented'); + } + + /** + * Set current page. + */ + public function setCurrent(string $path): void + { + throw new RuntimeException(__METHOD__ . '(): Not Implemented'); + } + + /** + * Return previous item. + * + * @return PageInterface|false + * @phpstan-return PageObject|false + */ + public function prev() + { + // FIXME: this method does not rewind the internal pointer! + $key = (string)$this->key(); + $prev = $this->prevSibling($key); + + return $prev !== $this->current() ? $prev : false; + } + + /** + * Return nth item. + * @param int $key + * @return PageInterface|bool + * @phpstan-return PageObject|false + */ + public function nth($key) + { + return $this->slice($key, 1)[0] ?? false; + } + + /** + * Pick one or more random entries. + * + * @param int $num Specifies how many entries should be picked. + * @return static + */ + public function random($num = 1) + { + return $this->createFrom($this->shuffle()->slice(0, $num)); + } + + /** + * Append new elements to the list. + * + * @param array $items Items to be appended. Existing keys will be overridden with the new values. + * @return static + */ + public function append($items) + { + throw new RuntimeException(__METHOD__ . '(): Not Implemented'); + } + + /** + * Split collection into array of smaller collections. + * + * @param int $size + * @return static[] + */ + public function batch($size): array + { + $chunks = $this->chunk($size); + + $list = []; + foreach ($chunks as $chunk) { + $list[] = $this->createFrom($chunk); + } + + return $list; + } + + /** + * Reorder collection. + * + * @param string $by + * @param string $dir + * @param array|null $manual + * @param int|null $sort_flags + * @return static + */ + public function order($by, $dir = 'asc', $manual = null, $sort_flags = null) + { + if (!$this->count()) { + return $this; + } + + if ($by === 'random') { + return $this->shuffle(); + } + + $keys = $this->buildSort($by, $dir, $manual, $sort_flags); + + return $this->createFrom(array_replace(array_flip($keys), $this->toArray()) ?? []); + } + + /** + * @param string $order_by + * @param string $order_dir + * @param array|null $manual + * @param int|null $sort_flags + * @return array + */ + protected function buildSort($order_by = 'default', $order_dir = 'asc', $manual = null, $sort_flags = null): array + { + // do this header query work only once + $header_query = null; + $header_default = null; + if (strpos($order_by, 'header.') === 0) { + $query = explode('|', str_replace('header.', '', $order_by), 2); + $header_query = array_shift($query) ?? ''; + $header_default = array_shift($query); + } + + $list = []; + foreach ($this as $key => $child) { + switch ($order_by) { + case 'title': + $list[$key] = $child->title(); + break; + case 'date': + $list[$key] = $child->date(); + $sort_flags = SORT_REGULAR; + break; + case 'modified': + $list[$key] = $child->modified(); + $sort_flags = SORT_REGULAR; + break; + case 'publish_date': + $list[$key] = $child->publishDate(); + $sort_flags = SORT_REGULAR; + break; + case 'unpublish_date': + $list[$key] = $child->unpublishDate(); + $sort_flags = SORT_REGULAR; + break; + case 'slug': + $list[$key] = $child->slug(); + break; + case 'basename': + $list[$key] = basename($key); + break; + case 'folder': + $list[$key] = $child->folder(); + break; + case 'manual': + case 'default': + default: + if (is_string($header_query)) { + /** @var Header $child_header */ + $child_header = $child->header(); + $header_value = $child_header->get($header_query); + if (is_array($header_value)) { + $list[$key] = implode(',', $header_value); + } elseif ($header_value) { + $list[$key] = $header_value; + } else { + $list[$key] = $header_default ?: $key; + } + $sort_flags = $sort_flags ?: SORT_REGULAR; + break; + } + $list[$key] = $key; + $sort_flags = $sort_flags ?: SORT_REGULAR; + } + } + + if (null === $sort_flags) { + $sort_flags = SORT_NATURAL | SORT_FLAG_CASE; + } + + // else just sort the list according to specified key + if (extension_loaded('intl') && Grav::instance()['config']->get('system.intl_enabled')) { + $locale = setlocale(LC_COLLATE, '0'); //`setlocale` with a '0' param returns the current locale set + $col = Collator::create($locale); + if ($col) { + $col->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON); + if (($sort_flags & SORT_NATURAL) === SORT_NATURAL) { + $list = preg_replace_callback('~([0-9]+)\.~', static function ($number) { + return sprintf('%032d.', $number[0]); + }, $list); + if (!is_array($list)) { + throw new RuntimeException('Internal Error'); + } + + $list_vals = array_values($list); + if (is_numeric(array_shift($list_vals))) { + $sort_flags = Collator::SORT_REGULAR; + } else { + $sort_flags = Collator::SORT_STRING; + } + } + + $col->asort($list, $sort_flags); + } else { + asort($list, $sort_flags); + } + } else { + asort($list, $sort_flags); + } + + // Move manually ordered items into the beginning of the list. Order of the unlisted items does not change. + if (is_array($manual) && !empty($manual)) { + $i = count($manual); + $new_list = []; + foreach ($list as $key => $dummy) { + $child = $this[$key] ?? null; + $order = $child ? array_search($child->slug, $manual, true) : false; + if ($order === false) { + $order = $i++; + } + $new_list[$key] = (int)$order; + } + + $list = $new_list; + + // Apply manual ordering to the list. + asort($list, SORT_NUMERIC); + } + + if ($order_dir !== 'asc') { + $list = array_reverse($list); + } + + return array_keys($list); + } + + /** + * Mimicks Pages class. + * + * @return $this + * @deprecated 1.7 Not needed anymore in Flex Pages (does nothing). + */ + public function all() + { + return $this; + } + + /** + * Returns the items between a set of date ranges of either the page date field (default) or + * an arbitrary datetime page field where start date and end date are optional + * Dates must be passed in as text that strtotime() can process + * http://php.net/manual/en/function.strtotime.php + * + * @param string|null $startDate + * @param string|null $endDate + * @param string|null $field + * @return static + * @throws Exception + */ + public function dateRange($startDate = null, $endDate = null, $field = null) + { + $start = $startDate ? Utils::date2timestamp($startDate) : null; + $end = $endDate ? Utils::date2timestamp($endDate) : null; + + $entries = []; + foreach ($this as $key => $object) { + if (!$object) { + continue; + } + + $date = $field ? strtotime($object->getNestedProperty($field)) : $object->date(); + + if ((!$start || $date >= $start) && (!$end || $date <= $end)) { + $entries[$key] = $object; + } + } + + return $this->createFrom($entries); + } + + /** + * Creates new collection with only visible pages + * + * @return static The collection with only visible pages + */ + public function visible() + { + $entries = []; + foreach ($this as $key => $object) { + if ($object && $object->visible()) { + $entries[$key] = $object; + } + } + + return $this->createFrom($entries); + } + + /** + * Creates new collection with only non-visible pages + * + * @return static The collection with only non-visible pages + */ + public function nonVisible() + { + $entries = []; + foreach ($this as $key => $object) { + if ($object && !$object->visible()) { + $entries[$key] = $object; + } + } + + return $this->createFrom($entries); + } + + /** + * Creates new collection with only pages + * + * @return static The collection with only pages + */ + public function pages() + { + $entries = []; + /** + * @var int|string $key + * @var PageInterface|null $object + */ + foreach ($this as $key => $object) { + if ($object && !$object->isModule()) { + $entries[$key] = $object; + } + } + + return $this->createFrom($entries); + } + + /** + * Creates new collection with only modules + * + * @return static The collection with only modules + */ + public function modules() + { + $entries = []; + /** + * @var int|string $key + * @var PageInterface|null $object + */ + foreach ($this as $key => $object) { + if ($object && $object->isModule()) { + $entries[$key] = $object; + } + } + + return $this->createFrom($entries); + } + + /** + * Alias of modules() + * + * @return static + */ + public function modular() + { + return $this->modules(); + } + + /** + * Alias of pages() + * + * @return static + */ + public function nonModular() + { + return $this->pages(); + } + + /** + * Creates new collection with only published pages + * + * @return static The collection with only published pages + */ + public function published() + { + $entries = []; + foreach ($this as $key => $object) { + if ($object && $object->published()) { + $entries[$key] = $object; + } + } + + return $this->createFrom($entries); + } + + /** + * Creates new collection with only non-published pages + * + * @return static The collection with only non-published pages + */ + public function nonPublished() + { + $entries = []; + foreach ($this as $key => $object) { + if ($object && !$object->published()) { + $entries[$key] = $object; + } + } + + return $this->createFrom($entries); + } + + /** + * Creates new collection with only routable pages + * + * @return static The collection with only routable pages + */ + public function routable() + { + $entries = []; + foreach ($this as $key => $object) { + if ($object && $object->routable()) { + $entries[$key] = $object; + } + } + + return $this->createFrom($entries); + } + + /** + * Creates new collection with only non-routable pages + * + * @return static The collection with only non-routable pages + */ + public function nonRoutable() + { + $entries = []; + foreach ($this as $key => $object) { + if ($object && !$object->routable()) { + $entries[$key] = $object; + } + } + + return $this->createFrom($entries); + } + + /** + * Creates new collection with only pages of the specified type + * + * @param string $type + * @return static The collection + */ + public function ofType($type) + { + $entries = []; + foreach ($this as $key => $object) { + if ($object && $object->template() === $type) { + $entries[$key] = $object; + } + } + + return $this->createFrom($entries); + } + + /** + * Creates new collection with only pages of one of the specified types + * + * @param string[] $types + * @return static The collection + */ + public function ofOneOfTheseTypes($types) + { + $entries = []; + foreach ($this as $key => $object) { + if ($object && in_array($object->template(), $types, true)) { + $entries[$key] = $object; + } + } + + return $this->createFrom($entries); + } + + /** + * Creates new collection with only pages of one of the specified access levels + * + * @param array $accessLevels + * @return static The collection + */ + public function ofOneOfTheseAccessLevels($accessLevels) + { + $entries = []; + foreach ($this as $key => $object) { + if ($object && isset($object->header()->access)) { + if (is_array($object->header()->access)) { + //Multiple values for access + $valid = false; + + foreach ($object->header()->access as $index => $accessLevel) { + if (is_array($accessLevel)) { + foreach ($accessLevel as $innerIndex => $innerAccessLevel) { + if (in_array($innerAccessLevel, $accessLevels)) { + $valid = true; + } + } + } else { + if (in_array($index, $accessLevels)) { + $valid = true; + } + } + } + if ($valid) { + $entries[$key] = $object; + } + } else { + //Single value for access + if (in_array($object->header()->access, $accessLevels)) { + $entries[$key] = $object; + } + } + } + } + + return $this->createFrom($entries); + } + + /** + * @param bool $bool + * @return static + */ + public function withOrdered(bool $bool = true) + { + $list = array_keys(array_filter($this->call('isOrdered', [$bool]))); + + return $this->select($list); + } + + /** + * @param bool $bool + * @return static + */ + public function withModules(bool $bool = true) + { + $list = array_keys(array_filter($this->call('isModule', [$bool]))); + + return $this->select($list); + } + + /** + * @param bool $bool + * @return static + */ + public function withPages(bool $bool = true) + { + $list = array_keys(array_filter($this->call('isPage', [$bool]))); + + return $this->select($list); + } + + /** + * @param bool $bool + * @param string|null $languageCode + * @param bool|null $fallback + * @return static + */ + public function withTranslation(bool $bool = true, string $languageCode = null, bool $fallback = null) + { + $list = array_keys(array_filter($this->call('hasTranslation', [$languageCode, $fallback]))); + + return $bool ? $this->select($list) : $this->unselect($list); + } + + /** + * @param string|null $languageCode + * @param bool|null $fallback + * @return PageIndex + */ + public function withTranslated(string $languageCode = null, bool $fallback = null) + { + return $this->getIndex()->withTranslated($languageCode, $fallback); + } + + /** + * Filter pages by given filters. + * + * - search: string + * - page_type: string|string[] + * - modular: bool + * - visible: bool + * - routable: bool + * - published: bool + * - page: bool + * - translated: bool + * + * @param array $filters + * @param bool $recursive + * @return static + */ + public function filterBy(array $filters, bool $recursive = false) + { + $list = array_keys(array_filter($this->call('filterBy', [$filters, $recursive]))); + + return $this->select($list); + } + + /** + * Get the extended version of this Collection with each page keyed by route + * + * @return array + * @throws Exception + */ + public function toExtendedArray(): array + { + $entries = []; + foreach ($this as $key => $object) { + if ($object) { + $entries[$object->route()] = $object->toArray(); + } + } + + return $entries; + } + + /** + * @param array $options + * @return array + */ + public function getLevelListing(array $options): array + { + /** @var PageIndex $index */ + $index = $this->getIndex(); + + return method_exists($index, 'getLevelListing') ? $index->getLevelListing($options) : []; + } +} diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php new file mode 100644 index 0000000..5a7d773 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php @@ -0,0 +1,1163 @@ + + * + * @method PageIndex withModules(bool $bool = true) + * @method PageIndex withPages(bool $bool = true) + * @method PageIndex withTranslation(bool $bool = true, string $languageCode = null, bool $fallback = null) + */ +class PageIndex extends FlexPageIndex implements PageCollectionInterface +{ + use FlexGravTrait; + use FlexIndexTrait; + + public const VERSION = parent::VERSION . '.5'; + public const ORDER_LIST_REGEX = '/(\/\d+)\.[^\/]+/u'; + public const PAGE_ROUTE_REGEX = '/\/\d+\./u'; + + /** @var PageObject|array */ + protected $_root; + /** @var array|null */ + protected $_params; + + /** + * @param array $entries + * @param FlexDirectory|null $directory + */ + public function __construct(array $entries = [], FlexDirectory $directory = null) + { + // Remove root if it's taken. + if (isset($entries[''])) { + $this->_root = $entries['']; + unset($entries['']); + } + + parent::__construct($entries, $directory); + } + + /** + * @param FlexStorageInterface $storage + * @return array + */ + public static function loadEntriesFromStorage(FlexStorageInterface $storage): array + { + // Load saved index. + $index = static::loadIndex($storage); + + $version = $index['version'] ?? 0; + $force = static::VERSION !== $version; + + // TODO: Following check flex index to be out of sync after some saves, disabled until better solution is found. + //$timestamp = $index['timestamp'] ?? 0; + //if (!$force && $timestamp && $timestamp > time() - 1) { + // return $index['index']; + //} + + // Load up to date index. + $entries = parent::loadEntriesFromStorage($storage); + + return static::updateIndexFile($storage, $index['index'], $entries, ['include_missing' => true, 'force_update' => $force]); + } + + /** + * @param string $key + * @return PageObject|null + */ + public function get($key) + { + if (mb_strpos($key, '|') !== false) { + [$key, $params] = explode('|', $key, 2); + } + + $element = parent::get($key); + if (null === $element) { + return null; + } + + if (isset($params)) { + $element = $element->getTranslation(ltrim($params, '.')); + } + + return $element; + } + + /** + * @return PageObject + */ + public function getRoot() + { + $root = $this->_root; + if (is_array($root)) { + $directory = $this->getFlexDirectory(); + $storage = $directory->getStorage(); + + $defaults = [ + 'header' => [ + 'routable' => false, + 'permissions' => [ + 'inherit' => false + ] + ] + ]; + + $row = $storage->readRows(['' => null])[''] ?? null; + if (null !== $row) { + if (isset($row['__ERROR'])) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $message = sprintf('Flex Pages: root page is broken in storage: %s', $row['__ERROR']); + + $debugger->addException(new RuntimeException($message)); + $debugger->addMessage($message, 'error'); + + $row = ['__META' => $root]; + } + + } else { + $row = ['__META' => $root]; + } + + $row = array_merge_recursive($defaults, $row); + + /** @var PageObject $root */ + $root = $this->getFlexDirectory()->createObject($row, '/', false); + $root->name('root.md'); + $root->root(true); + + $this->_root = $root; + } + + return $root; + } + + /** + * @param string|null $languageCode + * @param bool|null $fallback + * @return PageIndex + */ + public function withTranslated(string $languageCode = null, bool $fallback = null) + { + if (null === $languageCode) { + return $this; + } + + $entries = $this->translateEntries($this->getEntries(), $languageCode, $fallback); + $params = ['language' => $languageCode, 'language_fallback' => $fallback] + $this->getParams(); + + return $this->createFrom($entries)->setParams($params); + } + + /** + * @return string|null + */ + public function getLanguage(): ?string + { + return $this->_params['language'] ?? null; + } + + /** + * Get the collection params + * + * @return array + */ + public function getParams(): array + { + return $this->_params ?? []; + } + + /** + * Get the collection param + * + * @param string $name + * @return mixed + */ + public function getParam(string $name) + { + return $this->_params[$name] ?? null; + } + + /** + * Set parameters to the Collection + * + * @param array $params + * @return $this + */ + public function setParams(array $params) + { + $this->_params = $this->_params ? array_merge($this->_params, $params) : $params; + + return $this; + } + + /** + * Set a parameter to the Collection + * + * @param string $name + * @param mixed $value + * @return $this + */ + public function setParam(string $name, $value) + { + $this->_params[$name] = $value; + + return $this; + } + + /** + * Get the collection params + * + * @return array + */ + public function params(): array + { + return $this->getParams(); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getCacheKey() + */ + public function getCacheKey(): string + { + return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->getKeys()) . $this->getKeyField() . $this->getLanguage()); + } + + /** + * Filter pages by given filters. + * + * - search: string + * - page_type: string|string[] + * - modular: bool + * - visible: bool + * - routable: bool + * - published: bool + * - page: bool + * - translated: bool + * + * @param array $filters + * @param bool $recursive + * @return static + */ + public function filterBy(array $filters, bool $recursive = false) + { + if (!$filters) { + return $this; + } + + if ($recursive) { + return $this->__call('filterBy', [$filters, true]); + } + + $list = []; + $index = $this; + foreach ($filters as $key => $value) { + switch ($key) { + case 'search': + $index = $index->search((string)$value); + break; + case 'page_type': + if (!is_array($value)) { + $value = is_string($value) && $value !== '' ? explode(',', $value) : []; + } + $index = $index->ofOneOfTheseTypes($value); + break; + case 'routable': + $index = $index->withRoutable((bool)$value); + break; + case 'published': + $index = $index->withPublished((bool)$value); + break; + case 'visible': + $index = $index->withVisible((bool)$value); + break; + case 'module': + $index = $index->withModules((bool)$value); + break; + case 'page': + $index = $index->withPages((bool)$value); + break; + case 'folder': + $index = $index->withPages(!$value); + break; + case 'translated': + $index = $index->withTranslation((bool)$value); + break; + default: + $list[$key] = $value; + } + } + + return $list ? $index->filterByParent($list) : $index; + } + + /** + * @param array $filters + * @return static + */ + protected function filterByParent(array $filters) + { + /** @var static $index */ + $index = parent::filterBy($filters); + + return $index; + } + + /** + * @param array $options + * @return array + */ + public function getLevelListing(array $options): array + { + // Undocumented B/C + $order = $options['order'] ?? 'asc'; + if ($order === SORT_ASC) { + $options['order'] = 'asc'; + } elseif ($order === SORT_DESC) { + $options['order'] = 'desc'; + } + + $options += [ + 'field' => null, + 'route' => null, + 'leaf_route' => null, + 'sortby' => null, + 'order' => 'asc', + 'lang' => null, + 'filters' => [], + ]; + + $options['filters'] += [ + 'type' => ['root', 'dir'], + ]; + + $key = 'page.idx.lev.' . sha1(json_encode($options, JSON_THROW_ON_ERROR) . $this->getCacheKey()); + $checksum = $this->getCacheChecksum(); + + $cache = $this->getCache('object'); + + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + + $result = null; + try { + $cached = $cache->get($key); + $test = $cached[0] ?? null; + $result = $test === $checksum ? ($cached[1] ?? null) : null; + } catch (\Psr\SimpleCache\InvalidArgumentException $e) { + $debugger->addException($e); + } + + try { + if (null === $result) { + $result = $this->getLevelListingRecurse($options); + $cache->set($key, [$checksum, $result]); + } + } catch (\Psr\SimpleCache\InvalidArgumentException $e) { + $debugger->addException($e); + } + + return $result; + } + + /** + * @param array $entries + * @param string|null $keyField + * @return static + */ + protected function createFrom(array $entries, string $keyField = null) + { + /** @var static $index */ + $index = parent::createFrom($entries, $keyField); + $index->_root = $this->getRoot(); + + return $index; + } + + /** + * @param array $entries + * @param string $lang + * @param bool|null $fallback + * @return array + */ + protected function translateEntries(array $entries, string $lang, bool $fallback = null): array + { + $languages = $this->getFallbackLanguages($lang, $fallback); + foreach ($entries as $key => &$entry) { + // Find out which version of the page we should load. + $translations = $this->getLanguageTemplates((string)$key); + if (!$translations) { + // No translations found, is this a folder? + continue; + } + + // Find a translation. + $template = null; + foreach ($languages as $code) { + if (isset($translations[$code])) { + $template = $translations[$code]; + break; + } + } + + // We couldn't find a translation, remove entry from the list. + if (!isset($code, $template)) { + unset($entries['key']); + continue; + } + + // Get the main key without template and langauge. + [$main_key,] = explode('|', $entry['storage_key'] . '|', 2); + + // Update storage key and language. + $entry['storage_key'] = $main_key . '|' . $template . '.' . $code; + $entry['lang'] = $code; + } + unset($entry); + + return $entries; + } + + /** + * @return array + */ + protected function getLanguageTemplates(string $key): array + { + $meta = $this->getMetaData($key); + $template = $meta['template'] ?? 'folder'; + $translations = $meta['markdown'] ?? []; + $list = []; + foreach ($translations as $code => $search) { + if (isset($search[$template])) { + // Use main template if possible. + $list[$code] = $template; + } elseif (!empty($search)) { + // Fall back to first matching template. + $list[$code] = key($search); + } + } + + return $list; + } + + /** + * @param string|null $languageCode + * @param bool|null $fallback + * @return array + */ + protected function getFallbackLanguages(string $languageCode = null, bool $fallback = null): array + { + $fallback = $fallback ?? true; + if (!$fallback && null !== $languageCode) { + return [$languageCode]; + } + + $grav = Grav::instance(); + + /** @var Language $language */ + $language = $grav['language']; + $languageCode = $languageCode ?? ''; + if ($languageCode === '' && $fallback) { + return $language->getFallbackLanguages(null, true); + } + + return $fallback ? $language->getFallbackLanguages($languageCode, true) : [$languageCode]; + } + + /** + * @param array $options + * @return array + */ + protected function getLevelListingRecurse(array $options): array + { + $filters = $options['filters'] ?? []; + $field = $options['field']; + $route = $options['route']; + $leaf_route = $options['leaf_route']; + $sortby = $options['sortby']; + $order = $options['order']; + $language = $options['lang']; + + $status = 'error'; + $msg = null; + $response = []; + $children = null; + $sub_route = null; + $extra = null; + + // Handle leaf_route + $leaf = null; + if ($leaf_route && $route !== $leaf_route) { + $nodes = explode('/', $leaf_route); + $sub_route = '/' . implode('/', array_slice($nodes, 1, $options['level']++)); + $options['route'] = $sub_route; + + [$status,,$leaf,$extra] = $this->getLevelListingRecurse($options); + } + + // Handle no route, assume page tree root + if (!$route) { + $page = $this->getRoot(); + } else { + $page = $this->get(trim($route, '/')); + } + $path = $page ? $page->path() : null; + + if ($field) { + // Get forced filters from the field. + $blueprint = $page ? $page->getBlueprint() : $this->getFlexDirectory()->getBlueprint(); + $settings = $blueprint->schema()->getProperty($field); + $filters = array_merge([], $filters, $settings['filters'] ?? []); + } + + // Clean up filter. + $filter_type = (array)($filters['type'] ?? []); + unset($filters['type']); + $filters = array_filter($filters, static function($val) { return $val !== null && $val !== ''; }); + + if ($page) { + $status = 'success'; + $msg = 'PLUGIN_ADMIN.PAGE_ROUTE_FOUND'; + + if ($page->root() && (!$filter_type || in_array('root', $filter_type, true))) { + if ($field) { + $response[] = [ + 'name' => '', + 'value' => '/', + 'item-key' => '', + 'filename' => '.', + 'extension' => '', + 'type' => 'root', + 'modified' => $page->modified(), + 'size' => 0, + 'symlink' => false, + 'has-children' => false + ]; + } else { + $response[] = [ + 'item-key' => '-root-', + 'icon' => 'root', + 'title' => 'Root', // FIXME + 'route' => [ + 'display' => '<root>', // FIXME + 'raw' => '_root', + ], + 'modified' => $page->modified(), + 'extras' => [ + 'template' => $page->template(), + //'lang' => null, + //'translated' => null, + 'langs' => [], + 'published' => false, + 'visible' => false, + 'routable' => false, + 'tags' => ['root', 'non-routable'], + 'actions' => ['edit'], // FIXME + ] + ]; + } + } + + /** @var PageIndex $children */ + $children = $page->children()->getIndex(); + $selectedChildren = $children->filterBy($filters, true); + + /** @var Header $header */ + $header = $page->header(); + + if (!$field && $header->get('admin.children_display_order') === 'collection' && ($orderby = $header->get('content.order.by'))) { + // Use custom sorting by page header. + $sortby = $orderby; + $order = $header->get('content.order.dir', $order); + $custom = $header->get('content.order.custom'); + } + + if ($sortby) { + // Sort children. + $selectedChildren = $selectedChildren->order($sortby, $order, $custom ?? null); + } + + /** @var UserInterface|null $user */ + $user = Grav::instance()['user'] ?? null; + + /** @var PageObject $child */ + foreach ($selectedChildren as $child) { + $selected = $child->path() === $extra; + $includeChildren = is_array($leaf) && !empty($leaf) && $selected; + if ($field) { + $child_count = count($child->children()); + $payload = [ + 'name' => $child->menu(), + 'value' => $child->rawRoute(), + 'item-key' => basename($child->rawRoute() ?? ''), + 'filename' => $child->folder(), + 'extension' => $child->extension(), + 'type' => 'dir', + 'modified' => $child->modified(), + 'size' => $child_count, + 'symlink' => false, + 'has-children' => $child_count > 0 + ]; + } else { + $lang = $child->findTranslation($language) ?? 'n/a'; + /** @var PageObject $child */ + $child = $child->getTranslation($language) ?? $child; + + // TODO: all these features are independent from each other, we cannot just have one icon/color to catch all. + // TODO: maybe icon by home/modular/page/folder (or even from blueprints) and color by visibility etc.. + if ($child->home()) { + $icon = 'home'; + } elseif ($child->isModule()) { + $icon = 'modular'; + } elseif ($child->visible()) { + $icon = 'visible'; + } elseif ($child->isPage()) { + $icon = 'page'; + } else { + // TODO: add support + $icon = 'folder'; + } + $tags = [ + $child->published() ? 'published' : 'non-published', + $child->visible() ? 'visible' : 'non-visible', + $child->routable() ? 'routable' : 'non-routable' + ]; + $extras = [ + 'template' => $child->template(), + 'lang' => $lang ?: null, + 'translated' => $lang ? $child->hasTranslation($language, false) : null, + 'langs' => $child->getAllLanguages(true) ?: null, + 'published' => $child->published(), + 'published_date' => $this->jsDate($child->publishDate()), + 'unpublished_date' => $this->jsDate($child->unpublishDate()), + 'visible' => $child->visible(), + 'routable' => $child->routable(), + 'tags' => $tags, + 'actions' => $this->getListingActions($child, $user), + ]; + $extras = array_filter($extras, static function ($v) { + return $v !== null; + }); + $tmp = $child->children()->getIndex(); + $child_count = $tmp->count(); + $count = $filters ? $tmp->filterBy($filters, true)->count() : null; + $route = $child->getRoute(); + $route = $route ? ($route->toString(false) ?: '/') : ''; + $payload = [ + 'item-key' => htmlspecialchars(basename($child->rawRoute() ?? $child->getKey())), + 'icon' => $icon, + 'title' => htmlspecialchars($child->menu()), + 'route' => [ + 'display' => htmlspecialchars($route) ?: null, + 'raw' => htmlspecialchars($child->rawRoute()), + ], + 'modified' => $this->jsDate($child->modified()), + 'child_count' => $child_count ?: null, + 'count' => $count ?? null, + 'filters_hit' => $filters ? ($child->filterBy($filters, false) ?: null) : null, + 'extras' => $extras + ]; + $payload = array_filter($payload, static function ($v) { + return $v !== null; + }); + } + + // Add children if any + if ($includeChildren) { + $payload['children'] = array_values($leaf); + } + + $response[] = $payload; + } + } else { + $msg = 'PLUGIN_ADMIN.PAGE_ROUTE_NOT_FOUND'; + } + + if ($field) { + $temp_array = []; + foreach ($response as $index => $item) { + $temp_array[$item['type']][$index] = $item; + } + + $sorted = Utils::sortArrayByArray($temp_array, $filter_type); + $response = Utils::arrayFlatten($sorted); + } + + return [$status, $msg, $response, $path]; + } + + /** + * @param PageObject $object + * @param UserInterface $user + * @return array + */ + protected function getListingActions(PageObject $object, UserInterface $user): array + { + $actions = []; + if ($object->isAuthorized('read', null, $user)) { + $actions[] = 'preview'; + $actions[] = 'edit'; + } + if ($object->isAuthorized('update', null, $user)) { + $actions[] = 'copy'; + $actions[] = 'move'; + } + if ($object->isAuthorized('delete', null, $user)) { + $actions[] = 'delete'; + } + + return $actions; + } + + /** + * @param FlexStorageInterface $storage + * @return CompiledJsonFile|CompiledYamlFile|null + */ + protected static function getIndexFile(FlexStorageInterface $storage) + { + if (!method_exists($storage, 'isIndexed') || !$storage->isIndexed()) { + return null; + } + + // Load saved index file. + $grav = Grav::instance(); + $locator = $grav['locator']; + + $filename = $locator->findResource('user-data://flex/indexes/pages.json', true, true); + + return CompiledJsonFile::instance($filename); + } + + /** + * @param int|null $timestamp + * @return string|null + */ + private function jsDate(int $timestamp = null): ?string + { + if (!$timestamp) { + return null; + } + + $config = Grav::instance()['config']; + $dateFormat = $config->get('system.pages.dateformat.long'); + + return date($dateFormat, $timestamp) ?: null; + } + + /** + * Add a single page to a collection + * + * @param PageInterface $page + * @return PageCollection + */ + public function addPage(PageInterface $page) + { + return $this->getCollection()->addPage($page); + } + + /** + * + * Create a copy of this collection + * + * @return static + */ + public function copy() + { + return clone $this; + } + + /** + * + * Merge another collection with the current collection + * + * @param PageCollectionInterface $collection + * @return PageCollection + */ + public function merge(PageCollectionInterface $collection) + { + return $this->getCollection()->merge($collection); + } + + + /** + * Intersect another collection with the current collection + * + * @param PageCollectionInterface $collection + * @return PageCollection + */ + public function intersect(PageCollectionInterface $collection) + { + return $this->getCollection()->intersect($collection); + } + + /** + * Split collection into array of smaller collections. + * + * @param int $size + * @return PageCollection[] + */ + public function batch($size) + { + return $this->getCollection()->batch($size); + } + + /** + * Remove item from the list. + * + * @param string $key + * @return PageObject|null + * @throws InvalidArgumentException + */ + public function remove($key) + { + return $this->getCollection()->remove($key); + } + + /** + * Reorder collection. + * + * @param string $by + * @param string $dir + * @param array $manual + * @param string $sort_flags + * @return static + */ + public function order($by, $dir = 'asc', $manual = null, $sort_flags = null) + { + /** @var PageCollectionInterface $collection */ + $collection = $this->__call('order', [$by, $dir, $manual, $sort_flags]); + + return $collection; + } + + /** + * Check to see if this item is the first in the collection. + * + * @param string $path + * @return bool True if item is first. + */ + public function isFirst($path): bool + { + /** @var bool $result */ + $result = $this->__call('isFirst', [$path]); + + return $result; + + } + + /** + * Check to see if this item is the last in the collection. + * + * @param string $path + * @return bool True if item is last. + */ + public function isLast($path): bool + { + /** @var bool $result */ + $result = $this->__call('isLast', [$path]); + + return $result; + } + + /** + * Gets the previous sibling based on current position. + * + * @param string $path + * @return PageObject|null The previous item. + */ + public function prevSibling($path) + { + /** @var PageObject|null $result */ + $result = $this->__call('prevSibling', [$path]); + + return $result; + } + + /** + * Gets the next sibling based on current position. + * + * @param string $path + * @return PageObject|null The next item. + */ + public function nextSibling($path) + { + /** @var PageObject|null $result */ + $result = $this->__call('nextSibling', [$path]); + + return $result; + } + + /** + * Returns the adjacent sibling based on a direction. + * + * @param string $path + * @param int $direction either -1 or +1 + * @return PageObject|false The sibling item. + */ + public function adjacentSibling($path, $direction = 1) + { + /** @var PageObject|false $result */ + $result = $this->__call('adjacentSibling', [$path, $direction]); + + return $result; + } + + /** + * Returns the item in the current position. + * + * @param string $path the path the item + * @return int|null The index of the current page, null if not found. + */ + public function currentPosition($path): ?int + { + /** @var int|null $result */ + $result = $this->__call('currentPosition', [$path]); + + return $result; + } + + /** + * Returns the items between a set of date ranges of either the page date field (default) or + * an arbitrary datetime page field where start date and end date are optional + * Dates must be passed in as text that strtotime() can process + * http://php.net/manual/en/function.strtotime.php + * + * @param string|null $startDate + * @param string|null $endDate + * @param string|null $field + * @return static + * @throws Exception + */ + public function dateRange($startDate = null, $endDate = null, $field = null) + { + $collection = $this->__call('dateRange', [$startDate, $endDate, $field]); + + return $collection; + } + + /** + * Mimicks Pages class. + * + * @return $this + * @deprecated 1.7 Not needed anymore in Flex Pages (does nothing). + */ + public function all() + { + return $this; + } + + /** + * Creates new collection with only visible pages + * + * @return static The collection with only visible pages + */ + public function visible() + { + $collection = $this->__call('visible', []); + + return $collection; + } + + /** + * Creates new collection with only non-visible pages + * + * @return static The collection with only non-visible pages + */ + public function nonVisible() + { + $collection = $this->__call('nonVisible', []); + + return $collection; + } + + /** + * Creates new collection with only non-modular pages + * + * @return static The collection with only non-modular pages + */ + public function pages() + { + $collection = $this->__call('pages', []); + + return $collection; + } + + /** + * Creates new collection with only modular pages + * + * @return static The collection with only modular pages + */ + public function modules() + { + $collection = $this->__call('modules', []); + + return $collection; + } + + /** + * Creates new collection with only modular pages + * + * @return static The collection with only modular pages + */ + public function modular() + { + return $this->modules(); + } + + /** + * Creates new collection with only non-modular pages + * + * @return static The collection with only non-modular pages + */ + public function nonModular() + { + return $this->pages(); + } + + /** + * Creates new collection with only published pages + * + * @return static The collection with only published pages + */ + public function published() + { + $collection = $this->__call('published', []); + + return $collection; + } + + /** + * Creates new collection with only non-published pages + * + * @return static The collection with only non-published pages + */ + public function nonPublished() + { + $collection = $this->__call('nonPublished', []); + + return $collection; + } + + /** + * Creates new collection with only routable pages + * + * @return static The collection with only routable pages + */ + public function routable() + { + $collection = $this->__call('routable', []); + + return $collection; + } + + /** + * Creates new collection with only non-routable pages + * + * @return static The collection with only non-routable pages + */ + public function nonRoutable() + { + $collection = $this->__call('nonRoutable', []); + + return $collection; + } + + /** + * Creates new collection with only pages of the specified type + * + * @param string $type + * @return static The collection + */ + public function ofType($type) + { + $collection = $this->__call('ofType', []); + + return $collection; + } + + /** + * Creates new collection with only pages of one of the specified types + * + * @param string[] $types + * @return static The collection + */ + public function ofOneOfTheseTypes($types) + { + $collection = $this->__call('ofOneOfTheseTypes', []); + + return $collection; + } + + /** + * Creates new collection with only pages of one of the specified access levels + * + * @param array $accessLevels + * @return static The collection + */ + public function ofOneOfTheseAccessLevels($accessLevels) + { + $collection = $this->__call('ofOneOfTheseAccessLevels', []); + + return $collection; + } + + /** + * Converts collection into an array. + * + * @return array + */ + public function toArray() + { + return $this->getCollection()->toArray(); + } + + /** + * Get the extended version of this Collection with each page keyed by route + * + * @return array + * @throws Exception + */ + public function toExtendedArray() + { + return $this->getCollection()->toExtendedArray(); + } + +} diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageObject.php b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php new file mode 100644 index 0000000..4c83f95 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php @@ -0,0 +1,730 @@ + true, + 'full_order' => true, + 'filterBy' => true, + 'translated' => false, + ] + parent::getCachedMethods(); + } + + /** + * @return void + */ + public function initialize(): void + { + if (!$this->_initialized) { + Grav::instance()->fireEvent('onPageProcessed', new Event(['page' => $this])); + $this->_initialized = true; + } + } + + public function translated(): bool + { + return $this->translatedLanguages(true) ? true : false; + } + + /** + * @param string|array $query + * @return Route|null + */ + public function getRoute($query = []): ?Route + { + $path = $this->route(); + if (null === $path) { + return null; + } + + $route = RouteFactory::createFromString($path); + if ($lang = $route->getLanguage()) { + $grav = Grav::instance(); + if (!$grav['config']->get('system.languages.include_default_lang')) { + /** @var Language $language */ + $language = $grav['language']; + if ($lang === $language->getDefault()) { + $route = $route->withLanguage(''); + } + } + } + if (is_array($query)) { + foreach ($query as $key => $value) { + $route = $route->withQueryParam($key, $value); + } + } else { + $route = $route->withAddedPath($query); + } + + return $route; + } + + /** + * @inheritdoc PageInterface + */ + public function getFormValue(string $name, $default = null, string $separator = null) + { + $test = new stdClass(); + + $value = $this->pageContentValue($name, $test); + if ($value !== $test) { + return $value; + } + + switch ($name) { + case 'name': + // TODO: this should not be template! + return $this->getProperty('template'); + case 'route': + $filesystem = Filesystem::getInstance(false); + $key = $filesystem->dirname($this->hasKey() ? '/' . $this->getKey() : '/'); + return $key !== '/' ? $key : null; + case 'full_route': + return $this->hasKey() ? '/' . $this->getKey() : ''; + case 'full_order': + return $this->full_order(); + case 'lang': + return $this->getLanguage() ?? ''; + case 'translations': + return $this->getLanguages(); + } + + return parent::getFormValue($name, $default, $separator); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getCacheKey() + */ + public function getCacheKey(): string + { + $cacheKey = parent::getCacheKey(); + if ($cacheKey) { + /** @var Language $language */ + $language = Grav::instance()['language']; + $cacheKey .= '_' . $language->getActive(); + } + + return $cacheKey; + } + + /** + * @param array $variables + * @return array + */ + protected function onBeforeSave(array $variables) + { + $reorder = $variables[0] ?? true; + + $meta = $this->getMetaData(); + if (($meta['copy'] ?? false) === true) { + $this->folder = $this->getKey(); + } + + // Figure out storage path to the new route. + $parentKey = $this->getProperty('parent_key'); + if ($parentKey !== '') { + $parentRoute = $this->getProperty('route'); + + // Root page cannot be moved. + if ($this->root()) { + throw new RuntimeException(sprintf('Root page cannot be moved to %s', $parentRoute)); + } + + // Make sure page isn't being moved under itself. + $key = $this->getStorageKey(); + + /** @var PageObject|null $parent */ + $parent = $parentKey !== false ? $this->getFlexDirectory()->getObject($parentKey, 'storage_key') : null; + if (!$parent) { + // Page cannot be moved to non-existing location. + throw new RuntimeException(sprintf('Page /%s cannot be moved to non-existing path %s', $key, $parentRoute)); + } + + // TODO: make sure that the page doesn't exist yet if moved/copied. + } + + if ($reorder === true && !$this->root()) { + $reorder = $this->_reorder; + } + + // Force automatic reorder if item is supposed to be added to the last. + if (!is_array($reorder) && (int)$this->order() >= 999999) { + $reorder = []; + } + + // Reorder siblings. + $siblings = is_array($reorder) ? ($this->reorderSiblings($reorder) ?? []) : []; + + $data = $this->prepareStorage(); + unset($data['header']); + + foreach ($siblings as $sibling) { + $data = $sibling->prepareStorage(); + unset($data['header']); + } + + return ['reorder' => $reorder, 'siblings' => $siblings]; + } + + /** + * @param array $variables + * @return array + */ + protected function onSave(array $variables): array + { + /** @var PageCollection $siblings */ + $siblings = $variables['siblings']; + foreach ($siblings as $sibling) { + $sibling->save(false); + } + + return $variables; + } + + /** + * @param array $variables + */ + protected function onAfterSave(array $variables): void + { + $this->getFlexDirectory()->reloadIndex(); + } + + /** + * @param UserInterface|null $user + */ + public function check(UserInterface $user = null): void + { + parent::check($user); + + if ($user && $this->isMoved()) { + $parentKey = $this->getProperty('parent_key'); + + /** @var PageObject|null $parent */ + $parent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key'); + if (!$parent || !$parent->isAuthorized('create', null, $user)) { + throw new \RuntimeException('Forbidden', 403); + } + } + } + + /** + * @param array|bool $reorder + * @return FlexObject|FlexObjectInterface + */ + public function save($reorder = true) + { + $variables = $this->onBeforeSave(func_get_args()); + + // Backwards compatibility with older plugins. + $fireEvents = $reorder && $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true); + $grav = $this->getContainer(); + if ($fireEvents) { + $self = $this; + $grav->fireEvent('onAdminSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => &$self])); + if ($self !== $this) { + throw new RuntimeException('Switching Flex Page object during onAdminSave event is not supported! Please update plugin.'); + } + } + + /** @var static $instance */ + $instance = parent::save(); + $variables = $this->onSave($variables); + + $this->onAfterSave($variables); + + // Backwards compatibility with older plugins. + if ($fireEvents) { + $grav->fireEvent('onAdminAfterSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => $this])); + } + + // Reset original after save events have all been called. + $this->_originalObject = null; + + return $instance; + } + + /** + * @return PageObject + */ + public function delete() + { + $result = parent::delete(); + + // Backwards compatibility with older plugins. + $fireEvents = $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true); + if ($fireEvents) { + $this->getContainer()->fireEvent('onAdminAfterDelete', new Event(['object' => $this])); + } + + return $result; + } + + /** + * Prepare move page to new location. Moves also everything that's under the current page. + * + * You need to call $this->save() in order to perform the move. + * + * @param PageInterface $parent New parent page. + * @return $this + */ + public function move(PageInterface $parent) + { + if (!$parent instanceof FlexObjectInterface) { + throw new RuntimeException('Failed: Parent is not Flex Object'); + } + + $this->_reorder = []; + $this->setProperty('parent_key', $parent->getStorageKey()); + $this->storeOriginal(); + + return $this; + } + + /** + * @param UserInterface $user + * @param string $action + * @param string $scope + * @param bool $isMe + * @return bool|null + */ + protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool + { + // Special case: creating a new page means checking parent for its permissions. + if ($action === 'create' && !$this->exists()) { + $parent = $this->parent(); + if ($parent && method_exists($parent, 'isAuthorized')) { + return $parent->isAuthorized($action, $scope, $user); + } + + return false; + } + + return parent::isAuthorizedOverride($user, $action, $scope, $isMe); + } + + /** + * @return bool + */ + protected function isMoved(): bool + { + $storageKey = $this->getMasterKey(); + $filesystem = Filesystem::getInstance(false); + $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/'); + $newParentKey = $this->getProperty('parent_key'); + + return $this->exists() && $oldParentKey !== $newParentKey; + } + + /** + * @param array $ordering + * @return PageCollection|null + */ + protected function reorderSiblings(array $ordering) + { + $storageKey = $this->getMasterKey(); + $isMoved = $this->isMoved(); + $order = !$isMoved ? $this->order() : false; + if ($order !== false) { + $order = (int)$order; + } + + $parent = $this->parent(); + if (!$parent) { + throw new RuntimeException('Cannot reorder a page which has no parent'); + } + + /** @var PageCollection $siblings */ + $siblings = $parent->children(); + $siblings = $siblings->getCollection()->withOrdered(); + + // Handle special case where ordering isn't given. + if ($ordering === []) { + if ($order >= 999999) { + // Set ordering to point to be the last item, ignoring the object itself. + $order = 0; + foreach ($siblings as $sibling) { + if ($sibling->getKey() !== $this->getKey()) { + $order = max($order, (int)$sibling->order()); + } + } + $this->order($order + 1); + } + + // Do not change sibling ordering. + return null; + } + + $siblings = $siblings->orderBy(['order' => 'ASC']); + + if ($storageKey !== null) { + if ($order !== false) { + // Add current page back to the list if it's ordered. + $siblings->set($storageKey, $this); + } else { + // Remove old copy of the current page from the siblings list. + $siblings->remove($storageKey); + } + } + + // Add missing siblings into the end of the list, keeping the previous ordering between them. + foreach ($siblings as $sibling) { + $folder = (string)$sibling->getProperty('folder'); + $basename = preg_replace('|^\d+\.|', '', $folder); + if (!in_array($basename, $ordering, true)) { + $ordering[] = $basename; + } + } + + // Reorder. + $ordering = array_flip(array_values($ordering)); + $count = count($ordering); + foreach ($siblings as $sibling) { + $folder = (string)$sibling->getProperty('folder'); + $basename = preg_replace('|^\d+\.|', '', $folder); + $newOrder = $ordering[$basename] ?? null; + $newOrder = null !== $newOrder ? $newOrder + 1 : (int)$sibling->order() + $count; + $sibling->order($newOrder); + } + + $siblings = $siblings->orderBy(['order' => 'ASC']); + $siblings->removeElement($this); + + // If menu item was moved, just make it to be the last in order. + if ($isMoved && $this->order() !== false) { + $parentKey = $this->getProperty('parent_key'); + if ($parentKey === '') { + $newParent = $this->getFlexDirectory()->getIndex()->getRoot(); + } else { + $newParent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key'); + if (!$newParent instanceof PageInterface) { + throw new RuntimeException("New parent page '{$parentKey}' not found."); + } + } + /** @var PageCollection $newSiblings */ + $newSiblings = $newParent->children(); + $newSiblings = $newSiblings->getCollection()->withOrdered(); + $order = 0; + foreach ($newSiblings as $sibling) { + $order = max($order, (int)$sibling->order()); + } + $this->order($order + 1); + } + + return $siblings; + } + + /** + * @return string + */ + public function full_order(): string + { + $route = $this->path() . '/' . $this->folder(); + + return preg_replace(PageIndex::ORDER_LIST_REGEX, '\\1', $route) ?? $route; + } + + /** + * @param string $name + * @return Blueprint + */ + protected function doGetBlueprint(string $name = ''): Blueprint + { + try { + // Make sure that pages has been initialized. + Pages::getTypes(); + + // TODO: We need to move raw blueprint logic to Grav itself to remove admin dependency here. + if ($name === 'raw') { + // Admin RAW mode. + if ($this->isAdminSite()) { + /** @var Admin $admin */ + $admin = Grav::instance()['admin']; + + $template = $this->isModule() ? 'modular_raw' : ($this->root() ? 'root_raw' : 'raw'); + + return $admin->blueprints("admin/pages/{$template}"); + } + } + + $template = $this->getProperty('template') . ($name ? '.' . $name : ''); + + $blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages'); + } catch (RuntimeException $e) { + $template = 'default' . ($name ? '.' . $name : ''); + + $blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages'); + } + + $isNew = $blueprint->get('initialized', false) === false; + if ($isNew === true && $name === '') { + // Support onBlueprintCreated event just like in Pages::blueprints($template) + $blueprint->set('initialized', true); + $blueprint->setFilename($template); + + Grav::instance()->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $template])); + } + + return $blueprint; + } + + /** + * @param array $options + * @return array + */ + public function getLevelListing(array $options): array + { + $index = $this->getFlexDirectory()->getIndex(); + if (!is_callable([$index, 'getLevelListing'])) { + return []; + } + + // Deal with relative paths. + $initial = $options['initial'] ?? null; + $var = $initial ? 'leaf_route' : 'route'; + $route = $options[$var] ?? ''; + if ($route !== '' && !str_starts_with($route, '/')) { + $filesystem = Filesystem::getInstance(); + + $route = "/{$this->getKey()}/{$route}"; + $route = $filesystem->normalize($route); + + $options[$var] = $route; + } + + [$status, $message, $response,] = $index->getLevelListing($options); + + return [$status, $message, $response, $options[$var] ?? null]; + } + + /** + * Filter page (true/false) by given filters. + * + * - search: string + * - extension: string + * - module: bool + * - visible: bool + * - routable: bool + * - published: bool + * - page: bool + * - translated: bool + * + * @param array $filters + * @param bool $recursive + * @return bool + */ + public function filterBy(array $filters, bool $recursive = false): bool + { + foreach ($filters as $key => $value) { + switch ($key) { + case 'search': + $matches = $this->search((string)$value) > 0.0; + break; + case 'page_type': + $types = $value ? explode(',', $value) : []; + $matches = in_array($this->template(), $types, true); + break; + case 'extension': + $matches = Utils::contains((string)$value, $this->extension()); + break; + case 'routable': + $matches = $this->isRoutable() === (bool)$value; + break; + case 'published': + $matches = $this->isPublished() === (bool)$value; + break; + case 'visible': + $matches = $this->isVisible() === (bool)$value; + break; + case 'module': + $matches = $this->isModule() === (bool)$value; + break; + case 'page': + $matches = $this->isPage() === (bool)$value; + break; + case 'folder': + $matches = $this->isPage() === !$value; + break; + case 'translated': + $matches = $this->hasTranslation() === (bool)$value; + break; + default: + $matches = true; + break; + } + + // If current filter does not match, we still may have match as a parent. + if ($matches === false) { + return $recursive && $this->children()->getIndex()->filterBy($filters, true)->count() > 0; + } + } + + return true; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::exists() + */ + public function exists(): bool + { + return $this->root ?: parent::exists(); + } + + /** + * @return array + */ + public function __debugInfo(): array + { + $list = parent::__debugInfo(); + + return $list + [ + '_content_meta:private' => $this->getContentMeta(), + '_content:private' => $this->getRawContent() + ]; + } + + /** + * @param array $elements + * @param bool $extended + */ + protected function filterElements(array &$elements, bool $extended = false): void + { + // Change parent page if needed. + if (array_key_exists('route', $elements) && isset($elements['folder'], $elements['name'])) { + $elements['template'] = $elements['name']; + + // Figure out storage path to the new route. + $parentKey = trim($elements['route'] ?? '', '/'); + if ($parentKey !== '') { + /** @var PageObject|null $parent */ + $parent = $this->getFlexDirectory()->getObject($parentKey); + $parentKey = $parent ? $parent->getStorageKey() : $parentKey; + } + + $elements['parent_key'] = $parentKey; + } + + // Deal with ordering=bool and order=page1,page2,page3. + if ($this->root()) { + // Root page doesn't have ordering. + unset($elements['ordering'], $elements['order']); + } elseif (array_key_exists('ordering', $elements) && array_key_exists('order', $elements)) { + // Store ordering. + $ordering = $elements['order'] ?? null; + $this->_reorder = !empty($ordering) ? explode(',', $ordering) : []; + + $order = false; + if ((bool)($elements['ordering'] ?? false)) { + $order = $this->order(); + if ($order === false) { + $order = 999999; + } + } + + $elements['order'] = $order; + } + + parent::filterElements($elements, true); + } + + /** + * @return array + */ + public function prepareStorage(): array + { + $meta = $this->getMetaData(); + $oldLang = $meta['lang'] ?? ''; + $newLang = $this->getProperty('lang') ?? ''; + + // Always clone the page to the new language. + if ($oldLang !== $newLang) { + $meta['clone'] = true; + } + + // Make sure that certain elements are always sent to the storage layer. + $elements = [ + '__META' => $meta, + 'storage_key' => $this->getStorageKey(), + 'parent_key' => $this->getProperty('parent_key'), + 'order' => $this->getProperty('order'), + 'folder' => preg_replace('|^\d+\.|', '', $this->getProperty('folder') ?? ''), + 'template' => preg_replace('|modular/|', '', $this->getProperty('template') ?? ''), + 'lang' => $newLang + ] + parent::prepareStorage(); + + return $elements; + } +} diff --git a/system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php b/system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php new file mode 100644 index 0000000..ba7fa06 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Pages/Storage/PageStorage.php @@ -0,0 +1,700 @@ +flags = FilesystemIterator::KEY_AS_FILENAME | FilesystemIterator::CURRENT_AS_FILEINFO + | FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS; + + $grav = Grav::instance(); + + $config = $grav['config']; + $this->ignore_hidden = (bool)$config->get('system.pages.ignore_hidden'); + $this->ignore_files = (array)$config->get('system.pages.ignore_files'); + $this->ignore_folders = (array)$config->get('system.pages.ignore_folders'); + $this->include_default_lang_file_extension = (bool)$config->get('system.languages.include_default_lang_file_extension', true); + $this->recurse = (bool)($options['recurse'] ?? true); + $this->regex = '/(\.([\w\d_-]+))?\.md$/D'; + } + + /** + * @param string $key + * @param bool $variations + * @return array + */ + public function parseKey(string $key, bool $variations = true): array + { + if (mb_strpos($key, '|') !== false) { + [$key, $params] = explode('|', $key, 2); + } else { + $params = ''; + } + $key = ltrim($key, '/'); + + $keys = parent::parseKey($key, false) + ['params' => $params]; + + if ($variations) { + $keys += $this->parseParams($key, $params); + } + + return $keys; + } + + /** + * @param string $key + * @return string + */ + public function readFrontmatter(string $key): string + { + $path = $this->getPathFromKey($key); + $file = $this->getFile($path); + try { + if ($file instanceof MarkdownFile) { + $frontmatter = $file->frontmatter(); + } else { + $frontmatter = $file->raw(); + } + } catch (RuntimeException $e) { + $frontmatter = 'ERROR: ' . $e->getMessage(); + } finally { + $file->free(); + unset($file); + } + + return $frontmatter; + } + + /** + * @param string $key + * @return string + */ + public function readRaw(string $key): string + { + $path = $this->getPathFromKey($key); + $file = $this->getFile($path); + try { + $raw = $file->raw(); + } catch (RuntimeException $e) { + $raw = 'ERROR: ' . $e->getMessage(); + } finally { + $file->free(); + unset($file); + } + + return $raw; + } + + /** + * @param array $keys + * @param bool $includeParams + * @return string + */ + public function buildStorageKey(array $keys, bool $includeParams = true): string + { + $key = $keys['key'] ?? null; + if (null === $key) { + $key = $keys['parent_key'] ?? ''; + if ($key !== '') { + $key .= '/'; + } + $order = $keys['order'] ?? null; + $folder = $keys['folder'] ?? 'undefined'; + $key .= is_numeric($order) ? sprintf('%02d.%s', $order, $folder) : $folder; + } + + $params = $includeParams ? $this->buildStorageKeyParams($keys) : ''; + + return $params ? "{$key}|{$params}" : $key; + } + + /** + * @param array $keys + * @return string + */ + public function buildStorageKeyParams(array $keys): string + { + $params = $keys['template'] ?? ''; + $language = $keys['lang'] ?? ''; + if ($language) { + $params .= '.' . $language; + } + + return $params; + } + + /** + * @param array $keys + * @return string + */ + public function buildFolder(array $keys): string + { + return $this->dataFolder . '/' . $this->buildStorageKey($keys, false); + } + + /** + * @param array $keys + * @return string + */ + public function buildFilename(array $keys): string + { + $file = $this->buildStorageKeyParams($keys); + + // Template is optional; if it is missing, we need to have to load the object metadata. + if ($file && $file[0] === '.') { + $meta = $this->getObjectMeta($this->buildStorageKey($keys, false)); + $file = ($meta['template'] ?? 'folder') . $file; + } + + return $file . $this->dataExt; + } + + /** + * @param array $keys + * @return string + */ + public function buildFilepath(array $keys): string + { + $folder = $this->buildFolder($keys); + $filename = $this->buildFilename($keys); + + return rtrim($folder, '/') !== $folder ? $folder . $filename : $folder . '/' . $filename; + } + + /** + * @param array $row + * @param bool $setDefaultLang + * @return array + */ + public function extractKeysFromRow(array $row, bool $setDefaultLang = true): array + { + $meta = $row['__META'] ?? null; + $storageKey = $row['storage_key'] ?? $meta['storage_key'] ?? ''; + $keyMeta = $storageKey !== '' ? $this->extractKeysFromStorageKey($storageKey) : null; + $parentKey = $row['parent_key'] ?? $meta['parent_key'] ?? $keyMeta['parent_key'] ?? ''; + $order = $row['order'] ?? $meta['order'] ?? $keyMeta['order'] ?? null; + $folder = $row['folder'] ?? $meta['folder'] ?? $keyMeta['folder'] ?? ''; + $template = $row['template'] ?? $meta['template'] ?? $keyMeta['template'] ?? ''; + $lang = $row['lang'] ?? $meta['lang'] ?? $keyMeta['lang'] ?? ''; + + // Handle default language, if it should be saved without language extension. + if ($setDefaultLang && empty($meta['markdown'][$lang])) { + $grav = Grav::instance(); + + /** @var Language $language */ + $language = $grav['language']; + $default = $language->getDefault(); + // Make sure that the default language file doesn't exist before overriding it. + if (empty($meta['markdown'][$default])) { + if ($this->include_default_lang_file_extension) { + if ($lang === '') { + $lang = $language->getDefault(); + } + } elseif ($lang === $language->getDefault()) { + $lang = ''; + } + } + } + + $keys = [ + 'key' => null, + 'params' => null, + 'parent_key' => $parentKey, + 'order' => is_numeric($order) ? (int)$order : null, + 'folder' => $folder, + 'template' => $template, + 'lang' => $lang + ]; + + $keys['key'] = $this->buildStorageKey($keys, false); + $keys['params'] = $this->buildStorageKeyParams($keys); + + return $keys; + } + + /** + * @param string $key + * @return array + */ + public function extractKeysFromStorageKey(string $key): array + { + if (mb_strpos($key, '|') !== false) { + [$key, $params] = explode('|', $key, 2); + [$template, $language] = mb_strpos($params, '.') !== false ? explode('.', $params, 2) : [$params, '']; + } else { + $params = $template = $language = ''; + } + $objectKey = basename($key); + if (preg_match('|^(\d+)\.(.+)$|', $objectKey, $matches)) { + [, $order, $folder] = $matches; + } else { + [$order, $folder] = ['', $objectKey]; + } + + $filesystem = Filesystem::getInstance(false); + + $parentKey = ltrim($filesystem->dirname('/' . $key), '/'); + + return [ + 'key' => $key, + 'params' => $params, + 'parent_key' => $parentKey, + 'order' => is_numeric($order) ? (int)$order : null, + 'folder' => $folder, + 'template' => $template, + 'lang' => $language + ]; + } + + /** + * @param string $key + * @param string $params + * @return array + */ + protected function parseParams(string $key, string $params): array + { + if (mb_strpos($params, '.') !== false) { + [$template, $language] = explode('.', $params, 2); + } else { + $template = $params; + $language = ''; + } + + if ($template === '') { + $meta = $this->getObjectMeta($key); + $template = $meta['template'] ?? 'folder'; + } + + return [ + 'file' => $template . ($language ? '.' . $language : ''), + 'template' => $template, + 'lang' => $language + ]; + } + + /** + * Prepares the row for saving and returns the storage key for the record. + * + * @param array $row + */ + protected function prepareRow(array &$row): void + { + // Remove keys used in the filesystem. + unset($row['parent_key'], $row['order'], $row['folder'], $row['template'], $row['lang']); + } + + /** + * @param string $key + * @return array + */ + protected function loadRow(string $key): ?array + { + $data = parent::loadRow($key); + + // Special case for root page. + if ($key === '' && null !== $data) { + $data['root'] = true; + } + + return $data; + } + + /** + * Page storage supports moving and copying the pages and their languages. + * + * $row['__META']['copy'] = true Use this if you want to copy the whole folder, otherwise it will be moved + * $row['__META']['clone'] = true Use this if you want to clone the file, otherwise it will be renamed + * + * @param string $key + * @param array $row + * @return array + */ + protected function saveRow(string $key, array $row): array + { + // Initialize all key-related variables. + $newKeys = $this->extractKeysFromRow($row); + $newKey = $this->buildStorageKey($newKeys); + $newFolder = $this->buildFolder($newKeys); + $newFilename = $this->buildFilename($newKeys); + $newFilepath = rtrim($newFolder, '/') !== $newFolder ? $newFolder . $newFilename : $newFolder . '/' . $newFilename; + + try { + if ($key === '' && empty($row['root'])) { + throw new RuntimeException('Page has no path'); + } + + $grav = Grav::instance(); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addMessage("Save page: {$newKey}", 'debug'); + + // Check if the row already exists. + $oldKey = $row['__META']['storage_key'] ?? null; + if (is_string($oldKey)) { + // Initialize all old key-related variables. + $oldKeys = $this->extractKeysFromRow(['__META' => $row['__META']], false); + $oldFolder = $this->buildFolder($oldKeys); + $oldFilename = $this->buildFilename($oldKeys); + + // Check if folder has changed. + if ($oldFolder !== $newFolder && file_exists($oldFolder)) { + $isCopy = $row['__META']['copy'] ?? false; + if ($isCopy) { + if (strpos($newFolder, $oldFolder . '/') === 0) { + throw new RuntimeException(sprintf('Page /%s cannot be copied to itself', $oldKey)); + } + + $this->copyRow($oldKey, $newKey); + $debugger->addMessage("Page copied: {$oldFolder} => {$newFolder}", 'debug'); + } else { + if (strpos($newFolder, $oldFolder . '/') === 0) { + throw new RuntimeException(sprintf('Page /%s cannot be moved to itself', $oldKey)); + } + + $this->renameRow($oldKey, $newKey); + $debugger->addMessage("Page moved: {$oldFolder} => {$newFolder}", 'debug'); + } + } + + // Check if filename has changed. + if ($oldFilename !== $newFilename) { + // Get instance of the old file (we have already copied/moved it). + $oldFilepath = "{$newFolder}/{$oldFilename}"; + $file = $this->getFile($oldFilepath); + + // Rename the file if we aren't supposed to clone it. + $isClone = $row['__META']['clone'] ?? false; + if (!$isClone && $file->exists()) { + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + $toPath = $locator->isStream($newFilepath) ? $locator->findResource($newFilepath, true, true) : GRAV_ROOT . "/{$newFilepath}"; + $success = $file->rename($toPath); + if (!$success) { + throw new RuntimeException("Changing page template failed: {$oldFilepath} => {$newFilepath}"); + } + $debugger->addMessage("Page template changed: {$oldFilename} => {$newFilename}", 'debug'); + } else { + $file = null; + $debugger->addMessage("Page template created: {$newFilename}", 'debug'); + } + } + } + + // Clean up the data to be saved. + $this->prepareRow($row); + unset($row['__META'], $row['__ERROR']); + + if (!isset($file)) { + $file = $this->getFile($newFilepath); + } + + // Compare existing file content to the new one and save the file only if content has been changed. + $file->free(); + $oldRaw = $file->raw(); + $file->content($row); + $newRaw = $file->raw(); + if ($oldRaw !== $newRaw) { + $file->save($row); + $debugger->addMessage("Page content saved: {$newFilepath}", 'debug'); + } else { + $debugger->addMessage('Page content has not been changed, do not update the file', 'debug'); + } + } catch (RuntimeException $e) { + $name = isset($file) ? $file->filename() : $newKey; + + throw new RuntimeException(sprintf('Flex saveRow(%s): %s', $name, $e->getMessage())); + } finally { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $locator->clearCache(); + + if (isset($file)) { + $file->free(); + unset($file); + } + } + + $row['__META'] = $this->getObjectMeta($newKey, true); + + return $row; + } + + /** + * Check if page folder should be deleted. + * + * Deleting page can be done either by deleting everything or just a single language. + * If key contains the language, delete only it, unless it is the last language. + * + * @param string $key + * @return bool + */ + protected function canDeleteFolder(string $key): bool + { + // Return true if there's no language in the key. + $keys = $this->extractKeysFromStorageKey($key); + if (!$keys['lang']) { + return true; + } + + // Get the main key and reload meta. + $key = $this->buildStorageKey($keys); + $meta = $this->getObjectMeta($key, true); + + // Return true if there aren't any markdown files left. + return empty($meta['markdown'] ?? []); + } + + /** + * Get key from the filesystem path. + * + * @param string $path + * @return string + */ + protected function getKeyFromPath(string $path): string + { + if ($this->base_path) { + $path = $this->base_path . '/' . $path; + } + + return $path; + } + + /** + * Returns list of all stored keys in [key => timestamp] pairs. + * + * @return array + */ + protected function buildIndex(): array + { + $this->clearCache(); + + return $this->getIndexMeta(); + } + + /** + * @param string $key + * @param bool $reload + * @return array + */ + protected function getObjectMeta(string $key, bool $reload = false): array + { + $keys = $this->extractKeysFromStorageKey($key); + $key = $keys['key']; + + if ($reload || !isset($this->meta[$key])) { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + if (mb_strpos($key, '@@') === false) { + $path = $this->getStoragePath($key); + if (is_string($path)) { + $path = $locator->isStream($path) ? $locator->findResource($path) : GRAV_ROOT . "/{$path}"; + } else { + $path = null; + } + } else { + $path = null; + } + + $modified = 0; + $markdown = []; + $children = []; + + if (is_string($path) && is_dir($path)) { + $modified = filemtime($path); + $iterator = new FilesystemIterator($path, $this->flags); + + /** @var SplFileInfo $info */ + foreach ($iterator as $k => $info) { + // Ignore all hidden files if set. + if ($k === '' || ($this->ignore_hidden && $k[0] === '.')) { + continue; + } + + if ($info->isDir()) { + // Ignore all folders in ignore list. + if ($this->ignore_folders && in_array($k, $this->ignore_folders, true)) { + continue; + } + + $children[$k] = false; + } else { + // Ignore all files in ignore list. + if ($this->ignore_files && in_array($k, $this->ignore_files, true)) { + continue; + } + + $timestamp = $info->getMTime(); + + // Page is the one that matches to $page_extensions list with the lowest index number. + if (preg_match($this->regex, $k, $matches)) { + $mark = $matches[2] ?? ''; + $ext = $matches[1] ?? ''; + $ext .= $this->dataExt; + $markdown[$mark][basename($k, $ext)] = $timestamp; + } + + $modified = max($modified, $timestamp); + } + } + } + + $rawRoute = trim(preg_replace(PageIndex::PAGE_ROUTE_REGEX, '/', "/{$key}") ?? '', '/'); + $route = PageIndex::normalizeRoute($rawRoute); + + ksort($markdown, SORT_NATURAL | SORT_FLAG_CASE); + ksort($children, SORT_NATURAL | SORT_FLAG_CASE); + + $file = array_key_first($markdown[''] ?? (reset($markdown) ?: [])); + + $meta = [ + 'key' => $route, + 'storage_key' => $key, + 'template' => $file, + 'storage_timestamp' => $modified, + ]; + if ($markdown) { + $meta['markdown'] = $markdown; + } + if ($children) { + $meta['children'] = $children; + } + $meta['checksum'] = md5(json_encode($meta) ?: ''); + + // Cache meta as copy. + $this->meta[$key] = $meta; + } else { + $meta = $this->meta[$key]; + } + + $params = $keys['params']; + if ($params) { + $language = $keys['lang']; + $template = $keys['template'] ?: array_key_first($meta['markdown'][$language]) ?? $meta['template']; + $meta['exists'] = ($template && !empty($meta['children'])) || isset($meta['markdown'][$language][$template]); + $meta['storage_key'] .= '|' . $params; + $meta['template'] = $template; + $meta['lang'] = $language; + } + + return $meta; + } + + /** + * @return array + */ + protected function getIndexMeta(): array + { + $queue = ['']; + $list = []; + do { + $current = array_pop($queue); + if ($current === null) { + break; + } + + $meta = $this->getObjectMeta($current); + $storage_key = $meta['storage_key']; + + if (!empty($meta['children'])) { + $prefix = $storage_key . ($storage_key !== '' ? '/' : ''); + + foreach ($meta['children'] as $child => $value) { + $queue[] = $prefix . $child; + } + } + + $list[$storage_key] = $meta; + } while ($queue); + + ksort($list, SORT_NATURAL | SORT_FLAG_CASE); + + // Update parent timestamps. + foreach (array_reverse($list) as $storage_key => $meta) { + if ($storage_key !== '') { + $filesystem = Filesystem::getInstance(false); + + $storage_key = (string)$storage_key; + $parentKey = $filesystem->dirname($storage_key); + if ($parentKey === '.') { + $parentKey = ''; + } + + /** @phpstan-var array{'storage_key': string, 'storage_timestamp': int, 'children': array} $parent */ + $parent = &$list[$parentKey]; + $basename = basename($storage_key); + + if (isset($parent['children'][$basename])) { + $timestamp = $meta['storage_timestamp']; + $parent['children'][$basename] = $timestamp; + if ($basename && $basename[0] === '_') { + $parent['storage_timestamp'] = max($parent['storage_timestamp'], $timestamp); + } + } + } + } + + return $list; + } + + /** + * @return string + */ + protected function getNewKey(): string + { + throw new RuntimeException('Generating random key is disabled for pages'); + } +} diff --git a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageContentTrait.php b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageContentTrait.php new file mode 100644 index 0000000..1bde7b0 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageContentTrait.php @@ -0,0 +1,75 @@ +getProperty($property) : null; + if (null === $value) { + $value = $this->language() . ($var ?? ($this->modified() . md5($this->filePath() ?? $this->getKey()))); + + $this->setProperty($property, $value); + if ($this->doHasProperty($property)) { + $value = $this->getProperty($property); + } + } + + return $value; + } + + + /** + * @inheritdoc + */ + public function date($var = null): int + { + return $this->loadHeaderProperty( + 'date', + $var, + function ($value) { + $value = $value ? Utils::date2timestamp($value, $this->getProperty('dateformat')) : false; + + if (!$value) { + // Get the specific translation updated date. + $meta = $this->getMetaData(); + $language = $meta['lang'] ?? ''; + $template = $this->getProperty('template'); + $value = $meta['markdown'][$language][$template] ?? 0; + } + + return $value ?: $this->modified(); + } + ); + } + + /** + * @inheritdoc + * @param bool $bool + */ + public function isPage(bool $bool = true): bool + { + $meta = $this->getMetaData(); + + return empty($meta['markdown']) !== $bool; + } +} diff --git a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.php b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.php new file mode 100644 index 0000000..589ea88 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.php @@ -0,0 +1,236 @@ +path() ?? ''; + + return $pages->children($path); + } + + /** + * Check to see if this item is the first in an array of sub-pages. + * + * @return bool True if item is first. + */ + public function isFirst(): bool + { + if (Utils::isAdminPlugin()) { + return parent::isFirst(); + } + + $path = $this->path(); + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; + if (null !== $path && $collection instanceof PageCollectionInterface) { + return $collection->isFirst($path); + } + + return true; + } + + /** + * Check to see if this item is the last in an array of sub-pages. + * + * @return bool True if item is last + */ + public function isLast(): bool + { + if (Utils::isAdminPlugin()) { + return parent::isLast(); + } + + $path = $this->path(); + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; + if (null !== $path && $collection instanceof PageCollectionInterface) { + return $collection->isLast($path); + } + + return true; + } + + /** + * Returns the adjacent sibling based on a direction. + * + * @param int $direction either -1 or +1 + * @return PageInterface|false the sibling page + */ + public function adjacentSibling($direction = 1) + { + if (Utils::isAdminPlugin()) { + return parent::adjacentSibling($direction); + } + + $path = $this->path(); + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; + if (null !== $path && $collection instanceof PageCollectionInterface) { + $child = $collection->adjacentSibling($path, $direction); + if ($child instanceof PageInterface) { + return $child; + } + } + + return false; + } + + /** + * Helper method to return an ancestor page. + * + * @param string|null $lookup Name of the parent folder + * @return PageInterface|null page you were looking for if it exists + */ + public function ancestor($lookup = null) + { + if (Utils::isAdminPlugin()) { + return parent::ancestor($lookup); + } + + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->ancestor($this->getProperty('parent_route'), $lookup); + } + + /** + * Method that contains shared logic for inherited() and inheritedField() + * + * @param string $field Name of the parent folder + * @return array + */ + protected function getInheritedParams($field): array + { + if (Utils::isAdminPlugin()) { + return parent::getInheritedParams($field); + } + + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + $inherited = $pages->inherited($this->getProperty('parent_route'), $field); + $inheritedParams = $inherited ? (array)$inherited->value('header.' . $field) : []; + $currentParams = (array)$this->getFormValue('header.' . $field); + if ($inheritedParams && is_array($inheritedParams)) { + $currentParams = array_replace_recursive($inheritedParams, $currentParams); + } + + return [$inherited, $currentParams]; + } + + /** + * Helper method to return a page. + * + * @param string $url the url of the page + * @param bool $all + * @return PageInterface|null page you were looking for if it exists + */ + public function find($url, $all = false) + { + if (Utils::isAdminPlugin()) { + return parent::find($url, $all); + } + + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->find($url, $all); + } + + /** + * Get a collection of pages in the current context. + * + * @param string|array $params + * @param bool $pagination + * @return PageCollectionInterface|Collection + * @throws InvalidArgumentException + */ + public function collection($params = 'content', $pagination = true) + { + if (Utils::isAdminPlugin()) { + return parent::collection($params, $pagination); + } + + if (is_string($params)) { + // Look into a page header field. + $params = (array)$this->getFormValue('header.' . $params); + } elseif (!is_array($params)) { + throw new InvalidArgumentException('Argument should be either header variable name or array of parameters'); + } + + $context = [ + 'pagination' => $pagination, + 'self' => $this + ]; + + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->getCollection($params, $context); + } + + /** + * @param string|array $value + * @param bool $only_published + * @return PageCollectionInterface|Collection + */ + public function evaluate($value, $only_published = true) + { + if (Utils::isAdminPlugin()) { + return parent::collection($value, $only_published); + } + + $params = [ + 'items' => $value, + 'published' => $only_published + ]; + $context = [ + 'event' => false, + 'pagination' => false, + 'url_taxonomy_filters' => false, + 'self' => $this + ]; + + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->getCollection($params, $context); + } +} diff --git a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php new file mode 100644 index 0000000..3b490a5 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageRoutableTrait.php @@ -0,0 +1,122 @@ +root()) { + return null; + } + + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + $filesystem = Filesystem::getInstance(false); + + // FIXME: this does not work, needs to use $pages->get() with cached parent id! + $key = $this->getKey(); + $parent_route = $filesystem->dirname('/' . $key); + + return $parent_route !== '/' ? $pages->find($parent_route) : $pages->root(); + } + + /** + * Returns the item in the current position. + * + * @return int|null the index of the current page. + */ + public function currentPosition(): ?int + { + $path = $this->path(); + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; + if (null !== $path && $collection instanceof PageCollectionInterface) { + return $collection->currentPosition($path); + } + + return 1; + } + + /** + * Returns whether or not this page is the currently active page requested via the URL. + * + * @return bool True if it is active + */ + public function active(): bool + { + $grav = Grav::instance(); + $uri_path = rtrim(urldecode($grav['uri']->path()), '/') ?: '/'; + $routes = $grav['pages']->routes(); + + return isset($routes[$uri_path]) && $routes[$uri_path] === $this->path(); + } + + /** + * Returns whether or not this URI's URL contains the URL of the active page. + * Or in other words, is this page's URL in the current URL + * + * @return bool True if active child exists + */ + public function activeChild(): bool + { + $grav = Grav::instance(); + /** @var Uri $uri */ + $uri = $grav['uri']; + /** @var Pages $pages */ + $pages = $grav['pages']; + $uri_path = rtrim(urldecode($uri->path()), '/'); + $routes = $pages->routes(); + + if (isset($routes[$uri_path])) { + $page = $pages->find($uri->route()); + /** @var PageInterface|null $child_page */ + $child_page = $page ? $page->parent() : null; + while ($child_page && !$child_page->root()) { + if ($this->path() === $child_page->path()) { + return true; + } + $child_page = $child_page->parent(); + } + } + + return false; + } +} diff --git a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php new file mode 100644 index 0000000..dff42c9 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageTranslateTrait.php @@ -0,0 +1,108 @@ +getLanguageTemplates(); + if (!$translated) { + return $translated; + } + + $grav = Grav::instance(); + + /** @var Language $language */ + $language = $grav['language']; + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + $languages = $language->getLanguages(); + $languages[] = ''; + $defaultCode = $language->getDefault(); + + if (isset($translated[$defaultCode])) { + unset($translated['']); + } + + foreach ($translated as $key => &$template) { + $template .= $key !== '' ? ".{$key}.md" : '.md'; + } + unset($template); + + $translated = array_intersect_key($translated, array_flip($languages)); + + $folder = $this->getStorageFolder(); + if (!$folder) { + return []; + } + $folder = $locator->isStream($folder) ? $locator->getResource($folder) : GRAV_ROOT . "/{$folder}"; + + $list = array_fill_keys($languages, null); + foreach ($translated as $languageCode => $languageFile) { + $languageExtension = $languageCode ? ".{$languageCode}.md" : '.md'; + $path = "{$folder}/{$languageFile}"; + + // FIXME: use flex, also rawRoute() does not fully work? + $aPage = new Page(); + $aPage->init(new SplFileInfo($path), $languageExtension); + if ($onlyPublished && !$aPage->published()) { + continue; + } + + $header = $aPage->header(); + // @phpstan-ignore-next-line + $routes = $header->routes ?? []; + $route = $routes['default'] ?? $aPage->rawRoute(); + if (!$route) { + $route = $aPage->route(); + } + + $list[$languageCode ?: $defaultCode] = $route ?? ''; + } + + $list = array_filter($list, static function ($var) { + return null !== $var; + }); + + // Hack to get the same result as with old pages. + foreach ($list as &$path) { + if ($path === '') { + $path = null; + } + } + + return $list; + } +} diff --git a/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupCollection.php b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupCollection.php new file mode 100644 index 0000000..93abbf8 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupCollection.php @@ -0,0 +1,56 @@ + + */ +class UserGroupCollection extends FlexCollection +{ + /** + * @return array + */ + public static function getCachedMethods(): array + { + return [ + 'authorize' => 'session', + ] + parent::getCachedMethods(); + } + + /** + * Checks user authorization to the action. + * + * @param string $action + * @param string|null $scope + * @return bool|null + */ + public function authorize(string $action, string $scope = null): ?bool + { + $authorized = null; + /** @var UserGroupObject $object */ + foreach ($this as $object) { + $auth = $object->authorize($action, $scope); + if ($auth === true) { + $authorized = true; + } elseif ($auth === false) { + return false; + } + } + + return $authorized; + } +} diff --git a/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupIndex.php b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupIndex.php new file mode 100644 index 0000000..4bee5ac --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupIndex.php @@ -0,0 +1,24 @@ + + */ +class UserGroupIndex extends FlexIndex +{ +} diff --git a/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php new file mode 100644 index 0000000..ea68fa1 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php @@ -0,0 +1,121 @@ + 'session', + ] + parent::getCachedMethods(); + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->getProperty('readableName'); + } + + /** + * Checks user authorization to the action. + * + * @param string $action + * @param string|null $scope + * @return bool|null + */ + public function authorize(string $action, string $scope = null): ?bool + { + if ($scope === 'test') { + $scope = null; + } elseif (!$this->getProperty('enabled', true)) { + return null; + } + + $access = $this->getAccess(); + + $authorized = $access->authorize($action, $scope); + if (is_bool($authorized)) { + return $authorized; + } + + return $access->authorize('admin.super') ? true : null; + } + + /** + * @return Access + */ + protected function getAccess(): Access + { + if (null === $this->_access) { + $this->getProperty('access'); + } + + return $this->_access; + } + + /** + * @param mixed $value + * @return array + */ + protected function offsetLoad_access($value): array + { + if (!$value instanceof Access) { + $value = new Access($value); + } + + $this->_access = $value; + + return $value->jsonSerialize(); + } + + /** + * @param mixed $value + * @return array + */ + protected function offsetPrepare_access($value): array + { + return $this->offsetLoad_access($value); + } + + /** + * @param array|null $value + * @return array|null + */ + protected function offsetSerialize_access(?array $value): ?array + { + return $value; + } +} diff --git a/system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php b/system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php new file mode 100644 index 0000000..f565c9f --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php @@ -0,0 +1,47 @@ +update($data)` instead (same but with data validation & filtering, file upload support). + */ + public function merge(array $data) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->update($data) method instead', E_USER_DEPRECATED); + + $this->setElements($this->getBlueprint()->mergeData($this->toArray(), $data)); + + return $this; + } + + /** + * Return media object for the User's avatar. + * + * @return ImageMedium|StaticImageMedium|null + * @deprecated 1.6 Use ->getAvatarImage() method instead. + */ + public function getAvatarMedia() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarImage() method instead', E_USER_DEPRECATED); + + return $this->getAvatarImage(); + } + + /** + * Return the User's avatar URL + * + * @return string + * @deprecated 1.6 Use ->getAvatarUrl() method instead. + */ + public function avatarUrl() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarUrl() method instead', E_USER_DEPRECATED); + + return $this->getAvatarUrl(); + } + + /** + * Checks user authorization to the action. + * Ensures backwards compatibility + * + * @param string $action + * @return bool + * @deprecated 1.5 Use ->authorize() method instead. + */ + public function authorise($action) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->authorize() method instead', E_USER_DEPRECATED); + + return $this->authorize($action) ?? false; + } + + /** + * Implements Countable interface. + * + * @return int + * @deprecated 1.6 Method makes no sense for user account. + */ + public function count() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED); + + return count($this->jsonSerialize()); + } +} diff --git a/system/src/Grav/Common/Flex/Types/Users/UserCollection.php b/system/src/Grav/Common/Flex/Types/Users/UserCollection.php new file mode 100644 index 0000000..9b6490a --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Users/UserCollection.php @@ -0,0 +1,135 @@ + + */ +class UserCollection extends FlexCollection implements UserCollectionInterface +{ + /** + * @return array + */ + public static function getCachedMethods(): array + { + return [ + 'authorize' => 'session', + ] + parent::getCachedMethods(); + } + + /** + * Load user account. + * + * Always creates user object. To check if user exists, use $this->exists(). + * + * @param string $username + * @return UserObject + */ + public function load($username): UserInterface + { + $username = (string)$username; + + if ($username !== '') { + $key = $this->filterUsername($username); + $user = $this->get($key); + if ($user) { + return $user; + } + } else { + $key = ''; + } + + $directory = $this->getFlexDirectory(); + + /** @var UserObject $object */ + $object = $directory->createObject( + [ + 'username' => $username, + 'state' => 'enabled' + ], + $key + ); + + return $object; + } + + /** + * Find a user by username, email, etc + * + * @param string $query the query to search for + * @param string|string[] $fields the fields to search + * @return UserObject + */ + public function find($query, $fields = ['username', 'email']): UserInterface + { + if (is_string($query) && $query !== '') { + foreach ((array)$fields as $field) { + if ($field === 'key') { + $user = $this->get($query); + } elseif ($field === 'storage_key') { + $user = $this->withKeyField('storage_key')->get($query); + } elseif ($field === 'flex_key') { + $user = $this->withKeyField('flex_key')->get($query); + } elseif ($field === 'username') { + $user = $this->get($this->filterUsername($query)); + } else { + $user = parent::find($query, $field); + } + if ($user instanceof UserObject) { + return $user; + } + } + } + + return $this->load(''); + } + + /** + * Delete user account. + * + * @param string $username + * @return bool True if user account was found and was deleted. + */ + public function delete($username): bool + { + $user = $this->load($username); + + $exists = $user->exists(); + if ($exists) { + $user->delete(); + } + + return $exists; + } + + /** + * @param string $key + * @return string + */ + protected function filterUsername(string $key): string + { + $storage = $this->getFlexDirectory()->getStorage(); + if (method_exists($storage, 'normalizeKey')) { + return $storage->normalizeKey($key); + } + + return mb_strtolower($key); + } +} diff --git a/system/src/Grav/Common/Flex/Types/Users/UserIndex.php b/system/src/Grav/Common/Flex/Types/Users/UserIndex.php new file mode 100644 index 0000000..6e0bc65 --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Users/UserIndex.php @@ -0,0 +1,204 @@ + + */ +class UserIndex extends FlexIndex implements UserCollectionInterface +{ + public const VERSION = parent::VERSION . '.1'; + + /** + * @param FlexStorageInterface $storage + * @return array + */ + public static function loadEntriesFromStorage(FlexStorageInterface $storage): array + { + // Load saved index. + $index = static::loadIndex($storage); + + $version = $index['version'] ?? 0; + $force = static::VERSION !== $version; + + // TODO: Following check flex index to be out of sync after some saves, disabled until better solution is found. + //$timestamp = $index['timestamp'] ?? 0; + //if (!$force && $timestamp && $timestamp > time() - 1) { + // return $index['index']; + //} + + // Load up to date index. + $entries = parent::loadEntriesFromStorage($storage); + + return static::updateIndexFile($storage, $index['index'], $entries, ['force_update' => $force]); + } + + /** + * @param array $meta + * @param array $data + * @param FlexStorageInterface $storage + * @return void + */ + public static function updateObjectMeta(array &$meta, array $data, FlexStorageInterface $storage): void + { + // Username can also be number and stored as such. + $key = (string)($data['username'] ?? $meta['key'] ?? $meta['storage_key']); + $meta['key'] = static::filterUsername($key, $storage); + $meta['email'] = isset($data['email']) ? mb_strtolower($data['email']) : null; + } + + /** + * Load user account. + * + * Always creates user object. To check if user exists, use $this->exists(). + * + * @param string $username + * @return UserObject + */ + public function load($username): UserInterface + { + $username = (string)$username; + + if ($username !== '') { + $key = static::filterUsername($username, $this->getFlexDirectory()->getStorage()); + $user = $this->get($key); + if ($user) { + return $user; + } + } else { + $key = ''; + } + + $directory = $this->getFlexDirectory(); + + /** @var UserObject $object */ + $object = $directory->createObject( + [ + 'username' => $username, + 'state' => 'enabled' + ], + $key + ); + + return $object; + } + + /** + * Delete user account. + * + * @param string $username + * @return bool True if user account was found and was deleted. + */ + public function delete($username): bool + { + $user = $this->load($username); + + $exists = $user->exists(); + if ($exists) { + $user->delete(); + } + + return $exists; + } + + /** + * Find a user by username, email, etc + * + * @param string $query the query to search for + * @param array $fields the fields to search + * @return UserObject + */ + public function find($query, $fields = ['username', 'email']): UserInterface + { + if (is_string($query) && $query !== '') { + foreach ((array)$fields as $field) { + if ($field === 'key') { + $user = $this->get($query); + } elseif ($field === 'storage_key') { + $user = $this->withKeyField('storage_key')->get($query); + } elseif ($field === 'flex_key') { + $user = $this->withKeyField('flex_key')->get($query); + } elseif ($field === 'email') { + $user = $this->withKeyField('email')->get($query); + } elseif ($field === 'username') { + $user = $this->get(static::filterUsername($query, $this->getFlexDirectory()->getStorage())); + } else { + $user = $this->__call('find', [$query, $field]); + } + if ($user) { + return $user; + } + } + } + + return $this->load(''); + } + + /** + * @param string $key + * @param FlexStorageInterface $storage + * @return string + */ + protected static function filterUsername(string $key, FlexStorageInterface $storage): string + { + return $storage->normalizeKey($key); + } + + /** + * @param FlexStorageInterface $storage + * @return CompiledYamlFile|null + */ + protected static function getIndexFile(FlexStorageInterface $storage) + { + // Load saved index file. + $grav = Grav::instance(); + $locator = $grav['locator']; + $filename = $locator->findResource('user-data://flex/indexes/accounts.yaml', true, true); + + return CompiledYamlFile::instance($filename); + } + + /** + * @param array $entries + * @param array $added + * @param array $updated + * @param array $removed + */ + protected static function onChanges(array $entries, array $added, array $updated, array $removed): void + { + $message = sprintf('Flex: User index updated, %d objects (%d added, %d updated, %d removed).', count($entries), count($added), count($updated), count($removed)); + + $grav = Grav::instance(); + + /** @var Logger $logger */ + $logger = $grav['log']; + $logger->addDebug($message); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addMessage($message, 'debug'); + } +} diff --git a/system/src/Grav/Common/Flex/Types/Users/UserObject.php b/system/src/Grav/Common/Flex/Types/Users/UserObject.php new file mode 100644 index 0000000..424e5eb --- /dev/null +++ b/system/src/Grav/Common/Flex/Types/Users/UserObject.php @@ -0,0 +1,951 @@ + 'session', + 'load' => false, + 'find' => false, + 'remove' => false, + 'get' => true, + 'set' => false, + 'undef' => false, + 'def' => false, + ] + parent::getCachedMethods(); + } + + /** + * UserObject constructor. + * @param array $elements + * @param string $key + * @param FlexDirectory $directory + * @param bool $validate + */ + public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false) + { + // User can only be authenticated via login. + unset($elements['authenticated'], $elements['authorized']); + + // Define username if it's not set. + if (!isset($elements['username'])) { + $storageKey = $elements['__META']['storage_key'] ?? null; + if (null !== $storageKey && $key === $directory->getStorage()->normalizeKey($storageKey)) { + $elements['username'] = $storageKey; + } else { + $elements['username'] = $key; + } + } + + // Define state if it isn't set. + if (!isset($elements['state'])) { + $elements['state'] = 'enabled'; + } + + parent::__construct($elements, $key, $directory, $validate); + } + + /** + * @return void + */ + public function onPrepareRegistration(): void + { + if (!$this->getProperty('access')) { + /** @var Config $config */ + $config = Grav::instance()['config']; + + $groups = $config->get('plugins.login.user_registration.groups', ''); + $access = $config->get('plugins.login.user_registration.access', ['site' => ['login' => true]]); + + $this->setProperty('groups', $groups); + $this->setProperty('access', $access); + } + } + + /** + * Helper to get content editor will fall back if not set + * + * @return string + */ + public function getContentEditor(): string + { + return $this->getProperty('content_editor', 'default'); + } + + /** + * Get value by using dot notation for nested arrays/objects. + * + * @example $value = $this->get('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string|null $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function get($name, $default = null, $separator = null) + { + return $this->getNestedProperty($name, $default, $separator); + } + + /** + * Set value by using dot notation for nested arrays/objects. + * + * @example $data->set('this.is.my.nested.variable', $value); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value New value. + * @param string|null $separator Separator, defaults to '.' + * @return $this + */ + public function set($name, $value, $separator = null) + { + $this->setNestedProperty($name, $value, $separator); + + return $this; + } + + /** + * Unset value by using dot notation for nested arrays/objects. + * + * @example $data->undef('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param string|null $separator Separator, defaults to '.' + * @return $this + */ + public function undef($name, $separator = null) + { + $this->unsetNestedProperty($name, $separator); + + return $this; + } + + /** + * Set default value by using dot notation for nested arrays/objects. + * + * @example $data->def('this.is.my.nested.variable', 'default'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string|null $separator Separator, defaults to '.' + * @return $this + */ + public function def($name, $default = null, $separator = null) + { + $this->defNestedProperty($name, $default, $separator); + + return $this; + } + + /** + * @return bool + */ + public function isMyself(): bool + { + $me = $this->getActiveUser(); + + return $me && $me->authenticated && $this->username === $me->username; + } + + /** + * Checks user authorization to the action. + * + * @param string $action + * @param string|null $scope + * @return bool|null + */ + public function authorize(string $action, string $scope = null): ?bool + { + if ($scope === 'test') { + // Special scope to test user permissions. + $scope = null; + } else { + // User needs to be enabled. + if ($this->getProperty('state') !== 'enabled') { + return false; + } + + // User needs to be logged in. + if (!$this->getProperty('authenticated')) { + return false; + } + + if (strpos($action, 'login') === false && !$this->getProperty('authorized')) { + // User needs to be authorized (2FA). + return false; + } + + // Workaround bug in Login::isUserAuthorizedForPage() <= Login v3.0.4 + if ((string)(int)$action === $action) { + return false; + } + } + + // Check custom application access. + $authorizeCallable = static::$authorizeCallable; + if ($authorizeCallable instanceof Closure) { + $authorizeCallable->bindTo($this); + $authorized = $authorizeCallable($action, $scope); + if (is_bool($authorized)) { + return $authorized; + } + } + + // Check user access. + $access = $this->getAccess(); + $authorized = $access->authorize($action, $scope); + if (is_bool($authorized)) { + return $authorized; + } + + // Check group access. + $authorized = $this->getGroups()->authorize($action, $scope); + if (is_bool($authorized)) { + return $authorized; + } + + // If any specific rule isn't hit, check if user is a superuser. + return $access->authorize('admin.super') === true; + } + + /** + * @param string $property + * @param mixed $default + * @return mixed + */ + public function getProperty($property, $default = null) + { + $value = parent::getProperty($property, $default); + + if ($property === 'avatar') { + $settings = $this->getMediaFieldSettings($property); + $value = $this->parseFileProperty($value, $settings); + } + + return $value; + } + + /** + * @return UserGroupIndex + */ + public function getRoles(): UserGroupIndex + { + return $this->getGroups(); + } + + /** + * Convert object into an array. + * + * @return array + */ + public function toArray() + { + $array = $this->jsonSerialize(); + + $settings = $this->getMediaFieldSettings('avatar'); + $array['avatar'] = $this->parseFileProperty($array['avatar'] ?? null, $settings); + + return $array; + } + + /** + * Convert object into YAML string. + * + * @param int $inline The level where you switch to inline YAML. + * @param int $indent The amount of spaces to use for indentation of nested nodes. + * @return string A YAML string representing the object. + */ + public function toYaml($inline = 5, $indent = 2) + { + $yaml = new YamlFormatter(['inline' => $inline, 'indent' => $indent]); + + return $yaml->encode($this->toArray()); + } + + /** + * Convert object into JSON string. + * + * @return string + */ + public function toJson() + { + $json = new JsonFormatter(); + + return $json->encode($this->toArray()); + } + + /** + * Join nested values together by using blueprints. + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value Value to be joined. + * @param string|null $separator Separator, defaults to '.' + * @return $this + * @throws RuntimeException + */ + public function join($name, $value, $separator = null) + { + $separator = $separator ?? '.'; + $old = $this->get($name, null, $separator); + if ($old !== null) { + if (!is_array($old)) { + throw new RuntimeException('Value ' . $old); + } + + if (is_object($value)) { + $value = (array) $value; + } elseif (!is_array($value)) { + throw new RuntimeException('Value ' . $value); + } + + $value = $this->getBlueprint()->mergeData($old, $value, $name, $separator); + } + + $this->set($name, $value, $separator); + + return $this; + } + + /** + * Get nested structure containing default values defined in the blueprints. + * + * Fields without default value are ignored in the list. + + * @return array + */ + public function getDefaults() + { + return $this->getBlueprint()->getDefaults(); + } + + /** + * Set default values by using blueprints. + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value Value to be joined. + * @param string|null $separator Separator, defaults to '.' + * @return $this + */ + public function joinDefaults($name, $value, $separator = null) + { + if (is_object($value)) { + $value = (array) $value; + } + + $old = $this->get($name, null, $separator); + if ($old !== null) { + $value = $this->getBlueprint()->mergeData($value, $old, $name, $separator ?? '.'); + } + + $this->setNestedProperty($name, $value, $separator); + + return $this; + } + + /** + * Get value from the configuration and join it with given data. + * + * @param string $name Dot separated path to the requested value. + * @param array|object $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return array + * @throws RuntimeException + */ + public function getJoined($name, $value, $separator = null) + { + if (is_object($value)) { + $value = (array) $value; + } elseif (!is_array($value)) { + throw new RuntimeException('Value ' . $value); + } + + $old = $this->get($name, null, $separator); + + if ($old === null) { + // No value set; no need to join data. + return $value; + } + + if (!is_array($old)) { + throw new RuntimeException('Value ' . $old); + } + + // Return joined data. + return $this->getBlueprint()->mergeData($old, $value, $name, $separator ?? '.'); + } + + /** + * Set default values to the configuration if variables were not set. + * + * @param array $data + * @return $this + */ + public function setDefaults(array $data) + { + $this->setElements($this->getBlueprint()->mergeData($data, $this->toArray())); + + return $this; + } + + /** + * Validate by blueprints. + * + * @return $this + * @throws \Exception + */ + public function validate() + { + $this->getBlueprint()->validate($this->toArray()); + + return $this; + } + + /** + * Filter all items by using blueprints. + * @return $this + */ + public function filter() + { + $this->setElements($this->getBlueprint()->filter($this->toArray())); + + return $this; + } + + /** + * Get extra items which haven't been defined in blueprints. + * + * @return array + */ + public function extra() + { + return $this->getBlueprint()->extra($this->toArray()); + } + + /** + * Return unmodified data as raw string. + * + * NOTE: This function only returns data which has been saved to the storage. + * + * @return string + */ + public function raw() + { + $file = $this->file(); + + return $file ? $file->raw() : ''; + } + + /** + * Set or get the data storage. + * + * @param FileInterface|null $storage Optionally enter a new storage. + * @return FileInterface|null + */ + public function file(FileInterface $storage = null) + { + if (null !== $storage) { + $this->_storage = $storage; + } + + return $this->_storage; + } + + /** + * @return bool + */ + public function isValid(): bool + { + return $this->getProperty('state') !== null; + } + + /** + * Save user + * + * @return static + */ + public function save() + { + // TODO: We may want to handle this in the storage layer in the future. + $key = $this->getStorageKey(); + if (!$key || strpos($key, '@@')) { + $storage = $this->getFlexDirectory()->getStorage(); + if ($storage instanceof FileStorage) { + $this->setStorageKey($this->getKey()); + } + } + + $password = $this->getProperty('password') ?? $this->getProperty('password1'); + if (null !== $password && '' !== $password) { + $password2 = $this->getProperty('password2'); + if (!\is_string($password) || ($password2 && $password !== $password2)) { + throw new \RuntimeException('Passwords did not match.'); + } + + $this->setProperty('hashed_password', Authentication::create($password)); + } + $this->unsetProperty('password'); + $this->unsetProperty('password1'); + $this->unsetProperty('password2'); + + // Backwards compatibility with older plugins. + $fireEvents = $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true); + $grav = $this->getContainer(); + if ($fireEvents) { + $self = $this; + $grav->fireEvent('onAdminSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => &$self])); + if ($self !== $this) { + throw new RuntimeException('Switching Flex User object during onAdminSave event is not supported! Please update plugin.'); + } + } + + $instance = parent::save(); + + // Backwards compatibility with older plugins. + if ($fireEvents) { + $grav->fireEvent('onAdminAfterSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => $this])); + } + + return $instance; + } + + /** + * @return array + */ + public function prepareStorage(): array + { + $elements = parent::prepareStorage(); + + // Do not save authorization information. + unset($elements['authenticated'], $elements['authorized']); + + return $elements; + } + + /** + * @return MediaCollectionInterface + */ + public function getMedia() + { + /** @var Media $media */ + $media = $this->getFlexMedia(); + + // Deal with shared avatar folder. + $path = $this->getAvatarFile(); + if ($path && !$media[$path] && is_file($path)) { + $medium = MediumFactory::fromFile($path); + if ($medium) { + $media->add($path, $medium); + $name = basename($path); + if ($name !== $path) { + $media->add($name, $medium); + } + } + } + + return $media; + } + + /** + * @return string|null + */ + public function getMediaFolder(): ?string + { + $folder = $this->getFlexMediaFolder(); + + // Check for shared media + if (!$folder && !$this->getFlexDirectory()->getMediaFolder()) { + $this->_loadMedia = false; + $folder = $this->getBlueprint()->fields()['avatar']['destination'] ?? 'user://accounts/avatars'; + } + + return $folder; + } + + /** + * @param string $name + * @return Blueprint + */ + protected function doGetBlueprint(string $name = ''): Blueprint + { + $blueprint = $this->getFlexDirectory()->getBlueprint($name ? '.' . $name : $name); + + // HACK: With folder storage we need to ignore the avatar destination. + if ($this->getFlexDirectory()->getMediaFolder()) { + $field = $blueprint->get('form/fields/avatar'); + if ($field) { + unset($field['destination']); + $blueprint->set('form/fields/avatar', $field); + } + } + + return $blueprint; + } + + /** + * @param UserInterface $user + * @param string $action + * @param string $scope + * @param bool $isMe + * @return bool|null + */ + protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe = false): ?bool + { + if ($user instanceof self && $user->getStorageKey() === $this->getStorageKey()) { + // User cannot delete his own account, otherwise he has full access. + return $action !== 'delete'; + } + + return parent::isAuthorizedOverride($user, $action, $scope, $isMe); + } + + /** + * @return string|null + */ + protected function getAvatarFile(): ?string + { + $avatars = $this->getElement('avatar'); + if (is_array($avatars) && $avatars) { + $avatar = array_shift($avatars); + + return $avatar['path'] ?? null; + } + + return null; + } + + /** + * Gets the associated media collection (original images). + * + * @return MediaCollectionInterface Representation of associated media. + */ + protected function getOriginalMedia() + { + $folder = $this->getMediaFolder(); + if ($folder) { + $folder .= '/original'; + } + + return (new Media($folder ?? '', $this->getMediaOrder()))->setTimestamps(); + } + + /** + * @param array $files + * @return void + */ + protected function setUpdatedMedia(array $files): void + { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + $media = $this->getMedia(); + if (!$media instanceof MediaUploadInterface) { + return; + } + + $filesystem = Filesystem::getInstance(false); + + $list = []; + $list_original = []; + foreach ($files as $field => $group) { + // Ignore files without a field. + if ($field === '') { + continue; + } + $field = (string)$field; + + // Load settings for the field. + $settings = $this->getMediaFieldSettings($field); + foreach ($group as $filename => $file) { + if ($file) { + // File upload. + $filename = $file->getClientFilename(); + + /** @var FormFlashFile $file */ + $data = $file->jsonSerialize(); + unset($data['tmp_name'], $data['path']); + } else { + // File delete. + $data = null; + } + + if ($file) { + // Check file upload against media limits (except for max size). + $filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings); + } + + $self = $settings['self']; + if ($this->_loadMedia && $self) { + $filepath = $filename; + } else { + $filepath = "{$settings['destination']}/{$filename}"; + + // For backwards compatibility we are always using relative path from the installation root. + if ($locator->isStream($filepath)) { + $filepath = $locator->findResource($filepath, false, true); + } + } + + // Special handling for original images. + if (strpos($field, '/original')) { + if ($this->_loadMedia && $self) { + $list_original[$filename] = [$file, $settings]; + } + continue; + } + + // Calculate path without the retina scaling factor. + $realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath)); + + $list[$filename] = [$file, $settings]; + + $path = str_replace('.', "\n", $field); + if (null !== $data) { + $data['name'] = $filename; + $data['path'] = $filepath; + + $this->setNestedProperty("{$path}\n{$realpath}", $data, "\n"); + } else { + $this->unsetNestedProperty("{$path}\n{$realpath}", "\n"); + } + } + } + + $this->clearMediaCache(); + + $this->_uploads = $list; + $this->_uploads_original = $list_original; + } + + protected function saveUpdatedMedia(): void + { + $media = $this->getMedia(); + if (!$media instanceof MediaUploadInterface) { + throw new RuntimeException('Internal error UO101'); + } + + // Upload/delete original sized images. + /** + * @var string $filename + * @var UploadedFileInterface|array|null $file + */ + foreach ($this->_uploads_original ?? [] as $filename => $file) { + $filename = 'original/' . $filename; + if (is_array($file)) { + [$file, $settings] = $file; + } else { + $settings = null; + } + if ($file instanceof UploadedFileInterface) { + $media->copyUploadedFile($file, $filename, $settings); + } else { + $media->deleteFile($filename, $settings); + } + } + + // Upload/delete altered files. + /** + * @var string $filename + * @var UploadedFileInterface|array|null $file + */ + foreach ($this->getUpdatedMedia() as $filename => $file) { + if (is_array($file)) { + [$file, $settings] = $file; + } else { + $settings = null; + } + if ($file instanceof UploadedFileInterface) { + $media->copyUploadedFile($file, $filename, $settings); + } else { + $media->deleteFile($filename, $settings); + } + } + + $this->setUpdatedMedia([]); + $this->clearMediaCache(); + } + + /** + * @return array + */ + protected function doSerialize(): array + { + return [ + 'type' => $this->getFlexType(), + 'key' => $this->getKey(), + 'elements' => $this->jsonSerialize(), + 'storage' => $this->getMetaData() + ]; + } + + /** + * @return UserGroupIndex + */ + protected function getUserGroups() + { + $grav = Grav::instance(); + + /** @var Flex $flex */ + $flex = $grav['flex']; + + /** @var UserGroupCollection|null $groups */ + $groups = $flex->getDirectory('user-groups'); + if ($groups) { + /** @var UserGroupIndex $index */ + $index = $groups->getIndex(); + + return $index; + } + + return $grav['user_groups']; + } + + /** + * @return UserGroupIndex + */ + protected function getGroups() + { + if (null === $this->_groups) { + $this->_groups = $this->getUserGroups()->select((array)$this->getProperty('groups')); + } + + return $this->_groups; + } + + /** + * @return Access + */ + protected function getAccess(): Access + { + if (null === $this->_access) { + $this->getProperty('access'); + } + + return $this->_access; + } + + /** + * @param mixed $value + * @return array + */ + protected function offsetLoad_access($value): array + { + if (!$value instanceof Access) { + $value = new Access($value); + } + + $this->_access = $value; + + return $value->jsonSerialize(); + } + + /** + * @param mixed $value + * @return array + */ + protected function offsetPrepare_access($value): array + { + return $this->offsetLoad_access($value); + } + + /** + * @param array|null $value + * @return array|null + */ + protected function offsetSerialize_access(?array $value): ?array + { + return $value; + } +} diff --git a/system/src/Grav/Common/Form/FormFlash.php b/system/src/Grav/Common/Form/FormFlash.php new file mode 100644 index 0000000..430f15e --- /dev/null +++ b/system/src/Grav/Common/Form/FormFlash.php @@ -0,0 +1,106 @@ +files as $field => $files) { + if (strpos($field, '/')) { + continue; + } + foreach ($files as $file) { + if (is_array($file)) { + $file['tmp_name'] = $this->getTmpDir() . '/' . $file['tmp_name']; + $fields[$field][$file['path'] ?? $file['name']] = $file; + } + } + } + + return $fields; + } + + /** + * @param string $field + * @param string $filename + * @param array $upload + * @return bool + * @deprecated 1.6 For backwards compatibility only, do not use + */ + public function uploadFile(string $field, string $filename, array $upload): bool + { + if (!$this->uniqueId) { + return false; + } + + $tmp_dir = $this->getTmpDir(); + Folder::create($tmp_dir); + + $tmp_file = $upload['file']['tmp_name']; + $basename = basename($tmp_file); + + if (!move_uploaded_file($tmp_file, $tmp_dir . '/' . $basename)) { + return false; + } + + $upload['file']['tmp_name'] = $basename; + $upload['file']['name'] = $filename; + + $this->addFileInternal($field, $filename, $upload['file']); + + return true; + } + + /** + * @param string $field + * @param string $filename + * @param array $upload + * @param array $crop + * @return bool + * @deprecated 1.6 For backwards compatibility only, do not use + */ + public function cropFile(string $field, string $filename, array $upload, array $crop): bool + { + if (!$this->uniqueId) { + return false; + } + + $tmp_dir = $this->getTmpDir(); + Folder::create($tmp_dir); + + $tmp_file = $upload['file']['tmp_name']; + $basename = basename($tmp_file); + + if (!move_uploaded_file($tmp_file, $tmp_dir . '/' . $basename)) { + return false; + } + + $upload['file']['tmp_name'] = $basename; + $upload['file']['name'] = $filename; + + $this->addFileInternal($field, $filename, $upload['file'], $crop); + + return true; + } +} diff --git a/system/src/Grav/Common/GPM/AbstractCollection.php b/system/src/Grav/Common/GPM/AbstractCollection.php new file mode 100644 index 0000000..b5b867e --- /dev/null +++ b/system/src/Grav/Common/GPM/AbstractCollection.php @@ -0,0 +1,41 @@ +toArray(), JSON_THROW_ON_ERROR); + } + + /** + * @return array + */ + public function toArray() + { + $items = []; + + foreach ($this->items as $name => $package) { + $items[$name] = $package->toArray(); + } + + return $items; + } +} diff --git a/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php new file mode 100644 index 0000000..5d6ad9f --- /dev/null +++ b/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php @@ -0,0 +1,50 @@ +items as $name => $package) { + $items[$name] = $package->toArray(); + } + + return json_encode($items, JSON_THROW_ON_ERROR); + } + + /** + * @return array + */ + public function toArray() + { + $items = []; + + foreach ($this->items as $name => $package) { + $items[$name] = $package->toArray(); + } + + return $items; + } +} diff --git a/system/src/Grav/Common/GPM/Common/CachedCollection.php b/system/src/Grav/Common/GPM/Common/CachedCollection.php new file mode 100644 index 0000000..172ba93 --- /dev/null +++ b/system/src/Grav/Common/GPM/Common/CachedCollection.php @@ -0,0 +1,43 @@ + $item) { + $this->append([$name => $item]); + } + } +} diff --git a/system/src/Grav/Common/GPM/Common/Package.php b/system/src/Grav/Common/GPM/Common/Package.php new file mode 100644 index 0000000..e06ebb6 --- /dev/null +++ b/system/src/Grav/Common/GPM/Common/Package.php @@ -0,0 +1,95 @@ +data = $package; + + if ($type) { + $this->data->set('package_type', $type); + } + } + + /** + * @return Data + */ + public function getData() + { + return $this->data; + } + + /** + * @param string $key + * @return mixed + */ + public function __get($key) + { + return $this->data->get($key); + } + + /** + * @param string $key + * @param mixed $value + * @return void + */ + public function __set($key, $value) + { + $this->data->set($key, $value); + } + + /** + * @param string $key + * @return bool + */ + public function __isset($key) + { + return isset($this->data->{$key}); + } + + /** + * @return string + */ + public function __toString() + { + return $this->toJson(); + } + + /** + * @return string + */ + public function toJson() + { + return $this->data->toJson(); + } + + /** + * @return array + */ + public function toArray() + { + return $this->data->toArray(); + } +} diff --git a/system/src/Grav/Common/GPM/GPM.php b/system/src/Grav/Common/GPM/GPM.php new file mode 100644 index 0000000..4505696 --- /dev/null +++ b/system/src/Grav/Common/GPM/GPM.php @@ -0,0 +1,1267 @@ + 'user/plugins/%name%', + 'themes' => 'user/themes/%name%', + 'skeletons' => 'user/' + ]; + + /** + * Creates a new GPM instance with Local and Remote packages available + * + * @param bool $refresh Applies to Remote Packages only and forces a refetch of data + * @param callable|null $callback Either a function or callback in array notation + */ + public function __construct($refresh = false, $callback = null) + { + parent::__construct(); + + Folder::create(CACHE_DIR . '/gpm'); + + $this->cache = []; + $this->installed = new Local\Packages(); + $this->refresh = $refresh; + $this->callback = $callback; + } + + /** + * Magic getter method + * + * @param string $offset Asset name value + * @return mixed Asset value + */ + public function __get($offset) + { + switch ($offset) { + case 'grav': + return $this->getGrav(); + } + + return parent::__get($offset); + } + + /** + * Magic method to determine if the attribute is set + * + * @param string $offset Asset name value + * @return bool True if the value is set + */ + public function __isset($offset) + { + switch ($offset) { + case 'grav': + return $this->getGrav() !== null; + } + + return parent::__isset($offset); + } + + /** + * Return the locally installed packages + * + * @return Local\Packages + */ + public function getInstalled() + { + return $this->installed; + } + + /** + * Returns the Locally installable packages + * + * @param array $list_type_installed + * @return array The installed packages + */ + public function getInstallable($list_type_installed = ['plugins' => true, 'themes' => true]) + { + $items = ['total' => 0]; + foreach ($list_type_installed as $type => $type_installed) { + if ($type_installed === false) { + continue; + } + $methodInstallableType = 'getInstalled' . ucfirst($type); + $to_install = $this->$methodInstallableType(); + $items[$type] = $to_install; + $items['total'] += count($to_install); + } + + return $items; + } + + /** + * Returns the amount of locally installed packages + * + * @return int Amount of installed packages + */ + public function countInstalled() + { + $installed = $this->getInstalled(); + + return count($installed['plugins']) + count($installed['themes']); + } + + /** + * Return the instance of a specific Package + * + * @param string $slug The slug of the Package + * @return Local\Package|null The instance of the Package + */ + public function getInstalledPackage($slug) + { + return $this->getInstalledPlugin($slug) ?? $this->getInstalledTheme($slug); + } + + /** + * Return the instance of a specific Plugin + * + * @param string $slug The slug of the Plugin + * @return Local\Package|null The instance of the Plugin + */ + public function getInstalledPlugin($slug) + { + return $this->installed['plugins'][$slug] ?? null; + } + + /** + * Returns the Locally installed plugins + * @return Iterator The installed plugins + */ + public function getInstalledPlugins() + { + return $this->installed['plugins']; + } + + + /** + * Returns the plugin's enabled state + * + * @param string $slug + * @return bool True if the Plugin is Enabled. False if manually set to enable:false. Null otherwise. + */ + public function isPluginEnabled($slug): bool + { + $grav = Grav::instance(); + + return ($grav['config']['plugins'][$slug]['enabled'] ?? false) === true; + } + + /** + * Checks if a Plugin is installed + * + * @param string $slug The slug of the Plugin + * @return bool True if the Plugin has been installed. False otherwise + */ + public function isPluginInstalled($slug): bool + { + return isset($this->installed['plugins'][$slug]); + } + + /** + * @param string $slug + * @return bool + */ + public function isPluginInstalledAsSymlink($slug) + { + $plugin = $this->getInstalledPlugin($slug); + + return (bool)($plugin->symlink ?? false); + } + + /** + * Return the instance of a specific Theme + * + * @param string $slug The slug of the Theme + * @return Local\Package|null The instance of the Theme + */ + public function getInstalledTheme($slug) + { + return $this->installed['themes'][$slug] ?? null; + } + + /** + * Returns the Locally installed themes + * + * @return Iterator The installed themes + */ + public function getInstalledThemes() + { + return $this->installed['themes']; + } + + /** + * Checks if a Theme is enabled + * + * @param string $slug The slug of the Theme + * @return bool True if the Theme has been set to the default theme. False if installed, but not enabled. Null otherwise. + */ + public function isThemeEnabled($slug): bool + { + $grav = Grav::instance(); + + $current_theme = $grav['config']['system']['pages']['theme'] ?? null; + + return $current_theme === $slug; + } + + /** + * Checks if a Theme is installed + * + * @param string $slug The slug of the Theme + * @return bool True if the Theme has been installed. False otherwise + */ + public function isThemeInstalled($slug): bool + { + return isset($this->installed['themes'][$slug]); + } + + /** + * Returns the amount of updates available + * + * @return int Amount of available updates + */ + public function countUpdates() + { + return count($this->getUpdatablePlugins()) + count($this->getUpdatableThemes()); + } + + /** + * Returns an array of Plugins and Themes that can be updated. + * Plugins and Themes are extended with the `available` property that relies to the remote version + * + * @param array $list_type_update specifies what type of package to update + * @return array Array of updatable Plugins and Themes. + * Format: ['total' => int, 'plugins' => array, 'themes' => array] + */ + public function getUpdatable($list_type_update = ['plugins' => true, 'themes' => true]) + { + $items = ['total' => 0]; + foreach ($list_type_update as $type => $type_updatable) { + if ($type_updatable === false) { + continue; + } + $methodUpdatableType = 'getUpdatable' . ucfirst($type); + $to_update = $this->$methodUpdatableType(); + $items[$type] = $to_update; + $items['total'] += count($to_update); + } + + return $items; + } + + /** + * Returns an array of Plugins that can be updated. + * The Plugins are extended with the `available` property that relies to the remote version + * + * @return array Array of updatable Plugins + */ + public function getUpdatablePlugins() + { + $items = []; + + $repository = $this->getRepository(); + if (null === $repository) { + return $items; + } + + $plugins = $repository['plugins']; + + // local cache to speed things up + if (isset($this->cache[__METHOD__])) { + return $this->cache[__METHOD__]; + } + + foreach ($this->installed['plugins'] as $slug => $plugin) { + if (!isset($plugins[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) { + continue; + } + + $local_version = $plugin->version ?? 'Unknown'; + $remote_version = $plugins[$slug]->version; + + if (version_compare($local_version, $remote_version) < 0) { + $plugins[$slug]->available = $remote_version; + $plugins[$slug]->version = $local_version; + $plugins[$slug]->type = $plugins[$slug]->release_type; + $items[$slug] = $plugins[$slug]; + } + } + + $this->cache[__METHOD__] = $items; + + return $items; + } + + /** + * Get the latest release of a package from the GPM + * + * @param string $package_name + * @return string|null + */ + public function getLatestVersionOfPackage($package_name) + { + $repository = $this->getRepository(); + if (null === $repository) { + return null; + } + + $plugins = $repository['plugins']; + if (isset($plugins[$package_name])) { + return $plugins[$package_name]->available ?: $plugins[$package_name]->version; + } + + //Not a plugin, it's a theme? + $themes = $repository['themes']; + if (isset($themes[$package_name])) { + return $themes[$package_name]->available ?: $themes[$package_name]->version; + } + + return null; + } + + /** + * Check if a Plugin or Theme is updatable + * + * @param string $slug The slug of the package + * @return bool True if updatable. False otherwise or if not found + */ + public function isUpdatable($slug) + { + return $this->isPluginUpdatable($slug) || $this->isThemeUpdatable($slug); + } + + /** + * Checks if a Plugin is updatable + * + * @param string $plugin The slug of the Plugin + * @return bool True if the Plugin is updatable. False otherwise + */ + public function isPluginUpdatable($plugin) + { + return array_key_exists($plugin, (array)$this->getUpdatablePlugins()); + } + + /** + * Returns an array of Themes that can be updated. + * The Themes are extended with the `available` property that relies to the remote version + * + * @return array Array of updatable Themes + */ + public function getUpdatableThemes() + { + $items = []; + + $repository = $this->getRepository(); + if (null === $repository) { + return $items; + } + + $themes = $repository['themes']; + + // local cache to speed things up + if (isset($this->cache[__METHOD__])) { + return $this->cache[__METHOD__]; + } + + foreach ($this->installed['themes'] as $slug => $plugin) { + if (!isset($themes[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) { + continue; + } + + $local_version = $plugin->version ?? 'Unknown'; + $remote_version = $themes[$slug]->version; + + if (version_compare($local_version, $remote_version) < 0) { + $themes[$slug]->available = $remote_version; + $themes[$slug]->version = $local_version; + $themes[$slug]->type = $themes[$slug]->release_type; + $items[$slug] = $themes[$slug]; + } + } + + $this->cache[__METHOD__] = $items; + + return $items; + } + + /** + * Checks if a Theme is Updatable + * + * @param string $theme The slug of the Theme + * @return bool True if the Theme is updatable. False otherwise + */ + public function isThemeUpdatable($theme) + { + return array_key_exists($theme, (array)$this->getUpdatableThemes()); + } + + /** + * Get the release type of a package (stable / testing) + * + * @param string $package_name + * @return string|null + */ + public function getReleaseType($package_name) + { + $repository = $this->getRepository(); + if (null === $repository) { + return null; + } + + $plugins = $repository['plugins']; + if (isset($plugins[$package_name])) { + return $plugins[$package_name]->release_type; + } + + //Not a plugin, it's a theme? + $themes = $repository['themes']; + if (isset($themes[$package_name])) { + return $themes[$package_name]->release_type; + } + + return null; + } + + /** + * Returns true if the package latest release is stable + * + * @param string $package_name + * @return bool + */ + public function isStableRelease($package_name) + { + return $this->getReleaseType($package_name) === 'stable'; + } + + /** + * Returns true if the package latest release is testing + * + * @param string $package_name + * @return bool + */ + public function isTestingRelease($package_name) + { + $package = $this->getInstalledPackage($package_name); + $testing = $package->testing ?? false; + + return $this->getReleaseType($package_name) === 'testing' || $testing; + } + + /** + * Returns a Plugin from the repository + * + * @param string $slug The slug of the Plugin + * @return Remote\Package|null Package if found, NULL if not + */ + public function getRepositoryPlugin($slug) + { + $packages = $this->getRepositoryPlugins(); + + return $packages ? ($packages[$slug] ?? null) : null; + } + + /** + * Returns the list of Plugins available in the repository + * + * @return Iterator|null The Plugins remotely available + */ + public function getRepositoryPlugins() + { + return $this->getRepository()['plugins'] ?? null; + } + + /** + * Returns a Theme from the repository + * + * @param string $slug The slug of the Theme + * @return Remote\Package|null Package if found, NULL if not + */ + public function getRepositoryTheme($slug) + { + $packages = $this->getRepositoryThemes(); + + return $packages ? ($packages[$slug] ?? null) : null; + } + + /** + * Returns the list of Themes available in the repository + * + * @return Iterator|null The Themes remotely available + */ + public function getRepositoryThemes() + { + return $this->getRepository()['themes'] ?? null; + } + + /** + * Returns the list of Plugins and Themes available in the repository + * + * @return Remote\Packages|null Available Plugins and Themes + * Format: ['plugins' => array, 'themes' => array] + */ + public function getRepository() + { + if (null === $this->repository) { + try { + $this->repository = new Remote\Packages($this->refresh, $this->callback); + } catch (Exception $e) {} + } + + return $this->repository; + } + + /** + * Returns Grav version available in the repository + * + * @return Remote\GravCore|null + */ + public function getGrav() + { + if (null === $this->grav) { + try { + $this->grav = new Remote\GravCore($this->refresh, $this->callback); + } catch (Exception $e) {} + } + + return $this->grav; + } + + /** + * Searches for a Package in the repository + * + * @param string $search Can be either the slug or the name + * @param bool $ignore_exception True if should not fire an exception (for use in Twig) + * @return Remote\Package|false Package if found, FALSE if not + */ + public function findPackage($search, $ignore_exception = false) + { + $search = strtolower($search); + + $found = $this->getRepositoryPlugin($search) ?? $this->getRepositoryTheme($search); + if ($found) { + return $found; + } + + $themes = $this->getRepositoryThemes(); + $plugins = $this->getRepositoryPlugins(); + + if (null === $themes || null === $plugins) { + if (!is_writable(GRAV_ROOT . '/cache/gpm')) { + throw new RuntimeException('The cache/gpm folder is not writable. Please check the folder permissions.'); + } + + if ($ignore_exception) { + return false; + } + + throw new RuntimeException('GPM not reachable. Please check your internet connection or check the Grav site is reachable'); + } + + foreach ($themes as $slug => $theme) { + if ($search === $slug || $search === $theme->name) { + return $theme; + } + } + + foreach ($plugins as $slug => $plugin) { + if ($search === $slug || $search === $plugin->name) { + return $plugin; + } + } + + return false; + } + + /** + * Download the zip package via the URL + * + * @param string $package_file + * @param string $tmp + * @return string|null + */ + public static function downloadPackage($package_file, $tmp) + { + $package = parse_url($package_file); + if (!is_array($package)) { + throw new \RuntimeException("Malformed GPM URL: {$package_file}"); + } + + $filename = basename($package['path'] ?? ''); + + if (Grav::instance()['config']->get('system.gpm.official_gpm_only') && ($package['host'] ?? null) !== 'getgrav.org') { + throw new RuntimeException('Only official GPM URLs are allowed. You can modify this behavior in the System configuration.'); + } + + $output = Response::get($package_file, []); + + if ($output) { + Folder::create($tmp); + file_put_contents($tmp . DS . $filename, $output); + return $tmp . DS . $filename; + } + + return null; + } + + /** + * Copy the local zip package to tmp + * + * @param string $package_file + * @param string $tmp + * @return string|null + */ + public static function copyPackage($package_file, $tmp) + { + $package_file = realpath($package_file); + + if ($package_file && file_exists($package_file)) { + $filename = basename($package_file); + Folder::create($tmp); + copy($package_file, $tmp . DS . $filename); + return $tmp . DS . $filename; + } + + return null; + } + + /** + * Try to guess the package type from the source files + * + * @param string $source + * @return string|false + */ + public static function getPackageType($source) + { + $plugin_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Plugin/m'; + $theme_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Theme/m'; + + if (file_exists($source . 'system/defines.php') && + file_exists($source . 'system/config/system.yaml') + ) { + return 'grav'; + } + + // must have a blueprint + if (!file_exists($source . 'blueprints.yaml')) { + return false; + } + + // either theme or plugin + $name = basename($source); + if (Utils::contains($name, 'theme')) { + return 'theme'; + } + if (Utils::contains($name, 'plugin')) { + return 'plugin'; + } + + $glob = glob($source . '*.php') ?: []; + foreach ($glob as $filename) { + $contents = file_get_contents($filename); + if (!$contents) { + continue; + } + if (preg_match($theme_regex, $contents)) { + return 'theme'; + } + if (preg_match($plugin_regex, $contents)) { + return 'plugin'; + } + } + + // Assume it's a theme + return 'theme'; + } + + /** + * Try to guess the package name from the source files + * + * @param string $source + * @return string|false + */ + public static function getPackageName($source) + { + $ignore_yaml_files = ['blueprints', 'languages']; + + $glob = glob($source . '*.yaml') ?: []; + foreach ($glob as $filename) { + $name = strtolower(basename($filename, '.yaml')); + if (in_array($name, $ignore_yaml_files)) { + continue; + } + + return $name; + } + + return false; + } + + /** + * Find/Parse the blueprint file + * + * @param string $source + * @return array|false + */ + public static function getBlueprints($source) + { + $blueprint_file = $source . 'blueprints.yaml'; + if (!file_exists($blueprint_file)) { + return false; + } + + $file = YamlFile::instance($blueprint_file); + $blueprint = (array)$file->content(); + $file->free(); + + return $blueprint; + } + + /** + * Get the install path for a name and a particular type of package + * + * @param string $type + * @param string $name + * @return string + */ + public static function getInstallPath($type, $name) + { + $locator = Grav::instance()['locator']; + + if ($type === 'theme') { + $install_path = $locator->findResource('themes://', false) . DS . $name; + } else { + $install_path = $locator->findResource('plugins://', false) . DS . $name; + } + + return $install_path; + } + + /** + * Searches for a list of Packages in the repository + * + * @param array $searches An array of either slugs or names + * @return array Array of found Packages + * Format: ['total' => int, 'not_found' => array, ] + */ + public function findPackages($searches = []) + { + $packages = ['total' => 0, 'not_found' => []]; + $inflector = new Inflector(); + + foreach ($searches as $search) { + $repository = ''; + // if this is an object, get the search data from the key + if (is_object($search)) { + $search = (array)$search; + $key = key($search); + $repository = $search[$key]; + $search = $key; + } + + $found = $this->findPackage($search); + if ($found) { + // set override repository if provided + if ($repository) { + $found->override_repository = $repository; + } + if (!isset($packages[$found->package_type])) { + $packages[$found->package_type] = []; + } + + $packages[$found->package_type][$found->slug] = $found; + $packages['total']++; + } else { + // make a best guess at the type based on the repo URL + if (Utils::contains($repository, '-theme')) { + $type = 'themes'; + } else { + $type = 'plugins'; + } + + $not_found = new stdClass(); + $not_found->name = $inflector::camelize($search); + $not_found->slug = $search; + $not_found->package_type = $type; + $not_found->install_path = str_replace('%name%', $search, $this->install_paths[$type]); + $not_found->override_repository = $repository; + $packages['not_found'][$search] = $not_found; + } + } + + return $packages; + } + + /** + * Return the list of packages that have the passed one as dependency + * + * @param string $slug The slug name of the package + * @return array + */ + public function getPackagesThatDependOnPackage($slug) + { + $plugins = $this->getInstalledPlugins(); + $themes = $this->getInstalledThemes(); + $packages = array_merge($plugins->toArray(), $themes->toArray()); + + $list = []; + foreach ($packages as $package_name => $package) { + $dependencies = $package['dependencies'] ?? []; + foreach ($dependencies as $dependency) { + if (is_array($dependency) && isset($dependency['name'])) { + $dependency = $dependency['name']; + } + + if ($dependency === $slug) { + $list[] = $package_name; + } + } + } + + return $list; + } + + + /** + * Get the required version of a dependency of a package + * + * @param string $package_slug + * @param string $dependency_slug + * @return mixed|null + */ + public function getVersionOfDependencyRequiredByPackage($package_slug, $dependency_slug) + { + $dependencies = $this->getInstalledPackage($package_slug)->dependencies ?? []; + foreach ($dependencies as $dependency) { + if (isset($dependency[$dependency_slug])) { + return $dependency[$dependency_slug]; + } + } + + return null; + } + + /** + * Check the package identified by $slug can be updated to the version passed as argument. + * Thrown an exception if it cannot be updated because another package installed requires it to be at an older version. + * + * @param string $slug + * @param string $version_with_operator + * @param array $ignore_packages_list + * @return bool + * @throws RuntimeException + */ + public function checkNoOtherPackageNeedsThisDependencyInALowerVersion($slug, $version_with_operator, $ignore_packages_list) + { + // check if any of the currently installed package need this in a lower version than the one we need. In case, abort and tell which package + $dependent_packages = $this->getPackagesThatDependOnPackage($slug); + $version = $this->calculateVersionNumberFromDependencyVersion($version_with_operator); + + if (count($dependent_packages)) { + foreach ($dependent_packages as $dependent_package) { + $other_dependency_version_with_operator = $this->getVersionOfDependencyRequiredByPackage($dependent_package, $slug); + $other_dependency_version = $this->calculateVersionNumberFromDependencyVersion($other_dependency_version_with_operator); + + // check version is compatible with the one needed by the current package + if ($this->versionFormatIsNextSignificantRelease($other_dependency_version_with_operator)) { + $compatible = $this->checkNextSignificantReleasesAreCompatible($version, $other_dependency_version); + if (!$compatible && !in_array($dependent_package, $ignore_packages_list, true)) { + throw new RuntimeException( + "Package $slug is required in an older version by package $dependent_package. This package needs a newer version, and because of this it cannot be installed. The $dependent_package package must be updated to use a newer release of $slug.", + 2 + ); + } + } + } + } + + return true; + } + + /** + * Check the passed packages list can be updated + * + * @param array $packages_names_list + * @return void + * @throws Exception + */ + public function checkPackagesCanBeInstalled($packages_names_list) + { + foreach ($packages_names_list as $package_name) { + $latest = $this->getLatestVersionOfPackage($package_name); + $this->checkNoOtherPackageNeedsThisDependencyInALowerVersion($package_name, $latest, $packages_names_list); + } + } + + /** + * Fetch the dependencies, check the installed packages and return an array with + * the list of packages with associated an information on what to do: install, update or ignore. + * + * `ignore` means the package is already installed and can be safely left as-is. + * `install` means the package is not installed and must be installed. + * `update` means the package is already installed and must be updated as a dependency needs a higher version. + * + * @param array $packages + * @return array + * @throws RuntimeException + */ + public function getDependencies($packages) + { + $dependencies = $this->calculateMergedDependenciesOfPackages($packages); + foreach ($dependencies as $dependency_slug => $dependencyVersionWithOperator) { + $dependency_slug = (string)$dependency_slug; + if (in_array($dependency_slug, $packages, true)) { + unset($dependencies[$dependency_slug]); + continue; + } + + // Check PHP version + if ($dependency_slug === 'php') { + $testVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator); + if (version_compare($testVersion, PHP_VERSION) === 1) { + //Needs a Grav update first + throw new RuntimeException("One of the packages require PHP {$dependencies['php']}. Please update PHP to resolve this"); + } + + unset($dependencies[$dependency_slug]); + continue; + } + + //First, check for Grav dependency. If a dependency requires Grav > the current version, abort and tell. + if ($dependency_slug === 'grav') { + $testVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator); + if (version_compare($testVersion, GRAV_VERSION) === 1) { + //Needs a Grav update first + throw new RuntimeException("One of the packages require Grav {$dependencies['grav']}. Please update Grav to the latest release."); + } + + unset($dependencies[$dependency_slug]); + continue; + } + + if ($this->isPluginInstalled($dependency_slug)) { + if ($this->isPluginInstalledAsSymlink($dependency_slug)) { + unset($dependencies[$dependency_slug]); + continue; + } + + $dependencyVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator); + + // get currently installed version + $locator = Grav::instance()['locator']; + $blueprints_path = $locator->findResource('plugins://' . $dependency_slug . DS . 'blueprints.yaml'); + $file = YamlFile::instance($blueprints_path); + $package_yaml = $file->content(); + $file->free(); + $currentlyInstalledVersion = $package_yaml['version']; + + // if requirement is next significant release, check is compatible with currently installed version, might not be + if ($this->versionFormatIsNextSignificantRelease($dependencyVersionWithOperator) + && $this->firstVersionIsLower($dependencyVersion, $currentlyInstalledVersion)) { + $compatible = $this->checkNextSignificantReleasesAreCompatible($dependencyVersion, $currentlyInstalledVersion); + + if (!$compatible) { + throw new RuntimeException( + 'Dependency ' . $dependency_slug . ' is required in an older version than the one installed. This package must be updated. Please get in touch with its developer.', + 2 + ); + } + } + + //if I already have the latest release, remove the dependency + $latestRelease = $this->getLatestVersionOfPackage($dependency_slug); + + if ($this->firstVersionIsLower($latestRelease, $dependencyVersion)) { + //throw an exception if a required version cannot be found in the GPM yet + throw new RuntimeException( + 'Dependency ' . $package_yaml['name'] . ' is required in version ' . $dependencyVersion . ' which is higher than the latest release, ' . $latestRelease . '. Try running `bin/gpm -f index` to force a refresh of the GPM cache', + 1 + ); + } + + if ($this->firstVersionIsLower($currentlyInstalledVersion, $dependencyVersion)) { + $dependencies[$dependency_slug] = 'update'; + } elseif ($currentlyInstalledVersion === $latestRelease) { + unset($dependencies[$dependency_slug]); + } else { + // an update is not strictly required mark as 'ignore' + $dependencies[$dependency_slug] = 'ignore'; + } + } else { + $dependencyVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator); + + // if requirement is next significant release, check is compatible with latest available version, might not be + if ($this->versionFormatIsNextSignificantRelease($dependencyVersionWithOperator)) { + $latestVersionOfPackage = $this->getLatestVersionOfPackage($dependency_slug); + if ($this->firstVersionIsLower($dependencyVersion, $latestVersionOfPackage)) { + $compatible = $this->checkNextSignificantReleasesAreCompatible( + $dependencyVersion, + $latestVersionOfPackage + ); + + if (!$compatible) { + throw new RuntimeException( + 'Dependency ' . $dependency_slug . ' is required in an older version than the latest release available, and it cannot be installed. This package must be updated. Please get in touch with its developer.', + 2 + ); + } + } + } + + $dependencies[$dependency_slug] = 'install'; + } + } + + $dependencies_slugs = array_keys($dependencies); + $this->checkNoOtherPackageNeedsTheseDependenciesInALowerVersion(array_merge($packages, $dependencies_slugs)); + + return $dependencies; + } + + /** + * @param array $dependencies_slugs + * @return void + */ + public function checkNoOtherPackageNeedsTheseDependenciesInALowerVersion($dependencies_slugs) + { + foreach ($dependencies_slugs as $dependency_slug) { + $this->checkNoOtherPackageNeedsThisDependencyInALowerVersion( + $dependency_slug, + $this->getLatestVersionOfPackage($dependency_slug), + $dependencies_slugs + ); + } + } + + /** + * @param string $firstVersion + * @param string $secondVersion + * @return bool + */ + private function firstVersionIsLower($firstVersion, $secondVersion) + { + return version_compare($firstVersion, $secondVersion) === -1; + } + + /** + * Calculates and merges the dependencies of a package + * + * @param string $packageName The package information + * @param array $dependencies The dependencies array + * @return array + */ + private function calculateMergedDependenciesOfPackage($packageName, $dependencies) + { + $packageData = $this->findPackage($packageName); + + if (empty($packageData->dependencies)) { + return $dependencies; + } + + foreach ($packageData->dependencies as $dependency) { + $dependencyName = $dependency['name'] ?? null; + if (!$dependencyName) { + continue; + } + + $dependencyVersion = $dependency['version'] ?? '*'; + + if (!isset($dependencies[$dependencyName])) { + // Dependency added for the first time + $dependencies[$dependencyName] = $dependencyVersion; + + //Factor in the package dependencies too + $dependencies = $this->calculateMergedDependenciesOfPackage($dependencyName, $dependencies); + } elseif ($dependencyVersion !== '*') { + // Dependency already added by another package + // If this package requires a version higher than the currently stored one, store this requirement instead + $currentDependencyVersion = $dependencies[$dependencyName]; + $currently_stored_version_number = $this->calculateVersionNumberFromDependencyVersion($currentDependencyVersion); + + $currently_stored_version_is_in_next_significant_release_format = false; + if ($this->versionFormatIsNextSignificantRelease($currentDependencyVersion)) { + $currently_stored_version_is_in_next_significant_release_format = true; + } + + if (!$currently_stored_version_number) { + $currently_stored_version_number = '*'; + } + + $current_package_version_number = $this->calculateVersionNumberFromDependencyVersion($dependencyVersion); + if (!$current_package_version_number) { + throw new RuntimeException("Bad format for version of dependency {$dependencyName} for package {$packageName}", 1); + } + + $current_package_version_is_in_next_significant_release_format = false; + if ($this->versionFormatIsNextSignificantRelease($dependencyVersion)) { + $current_package_version_is_in_next_significant_release_format = true; + } + + //If I had stored '*', change right away with the more specific version required + if ($currently_stored_version_number === '*') { + $dependencies[$dependencyName] = $dependencyVersion; + } elseif (!$currently_stored_version_is_in_next_significant_release_format && !$current_package_version_is_in_next_significant_release_format) { + //Comparing versions equals or higher, a simple version_compare is enough + if (version_compare($currently_stored_version_number, $current_package_version_number) === -1) { + //Current package version is higher + $dependencies[$dependencyName] = $dependencyVersion; + } + } else { + $compatible = $this->checkNextSignificantReleasesAreCompatible($currently_stored_version_number, $current_package_version_number); + if (!$compatible) { + throw new RuntimeException("Dependency {$dependencyName} is required in two incompatible versions", 2); + } + } + } + } + + return $dependencies; + } + + /** + * Calculates and merges the dependencies of the passed packages + * + * @param array $packages + * @return array + */ + public function calculateMergedDependenciesOfPackages($packages) + { + $dependencies = []; + + foreach ($packages as $package) { + $dependencies = $this->calculateMergedDependenciesOfPackage($package, $dependencies); + } + + return $dependencies; + } + + /** + * Returns the actual version from a dependency version string. + * Examples: + * $versionInformation == '~2.0' => returns '2.0' + * $versionInformation == '>=2.0.2' => returns '2.0.2' + * $versionInformation == '2.0.2' => returns '2.0.2' + * $versionInformation == '*' => returns null + * $versionInformation == '' => returns null + * + * @param string $version + * @return string|null + */ + public function calculateVersionNumberFromDependencyVersion($version) + { + if ($version === '*') { + return null; + } + if ($version === '') { + return null; + } + if ($this->versionFormatIsNextSignificantRelease($version)) { + return trim(substr($version, 1)); + } + if ($this->versionFormatIsEqualOrHigher($version)) { + return trim(substr($version, 2)); + } + + return $version; + } + + /** + * Check if the passed version information contains next significant release (tilde) operator + * + * Example: returns true for $version: '~2.0' + * + * @param string $version + * @return bool + */ + public function versionFormatIsNextSignificantRelease($version): bool + { + return strpos($version, '~') === 0; + } + + /** + * Check if the passed version information contains equal or higher operator + * + * Example: returns true for $version: '>=2.0' + * + * @param string $version + * @return bool + */ + public function versionFormatIsEqualOrHigher($version): bool + { + return strpos($version, '>=') === 0; + } + + /** + * Check if two releases are compatible by next significant release + * + * ~1.2 is equivalent to >=1.2 <2.0.0 + * ~1.2.3 is equivalent to >=1.2.3 <1.3.0 + * + * In short, allows the last digit specified to go up + * + * @param string $version1 the version string (e.g. '2.0.0' or '1.0') + * @param string $version2 the version string (e.g. '2.0.0' or '1.0') + * @return bool + */ + public function checkNextSignificantReleasesAreCompatible($version1, $version2): bool + { + $version1array = explode('.', $version1); + $version2array = explode('.', $version2); + + if (count($version1array) > count($version2array)) { + [$version1array, $version2array] = [$version2array, $version1array]; + } + + $i = 0; + while ($i < count($version1array) - 1) { + if ($version1array[$i] !== $version2array[$i]) { + return false; + } + $i++; + } + + return true; + } +} diff --git a/system/src/Grav/Common/GPM/Installer.php b/system/src/Grav/Common/GPM/Installer.php new file mode 100644 index 0000000..639240b --- /dev/null +++ b/system/src/Grav/Common/GPM/Installer.php @@ -0,0 +1,543 @@ + true, + 'ignore_symlinks' => true, + 'sophisticated' => false, + 'theme' => false, + 'install_path' => '', + 'ignores' => [], + 'exclude_checks' => [self::EXISTS, self::NOT_FOUND, self::IS_LINK] + ]; + + /** + * Installs a given package to a given destination. + * + * @param string $zip the local path to ZIP package + * @param string $destination The local path to the Grav Instance + * @param array $options Options to use for installing. ie, ['install_path' => 'user/themes/antimatter'] + * @param string|null $extracted The local path to the extacted ZIP package + * @param bool $keepExtracted True if you want to keep the original files + * @return bool True if everything went fine, False otherwise. + */ + public static function install($zip, $destination, $options = [], $extracted = null, $keepExtracted = false) + { + $destination = rtrim($destination, DS); + $options = array_merge(self::$options, $options); + $install_path = rtrim($destination . DS . ltrim($options['install_path'], DS), DS); + + if (!self::isGravInstance($destination) || !self::isValidDestination( + $install_path, + $options['exclude_checks'] + ) + ) { + return false; + } + + if ((self::lastErrorCode() === self::IS_LINK && $options['ignore_symlinks']) || + (self::lastErrorCode() === self::EXISTS && !$options['overwrite']) + ) { + return false; + } + + // Create a tmp location + $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true); + $tmp = $tmp_dir . '/Grav-' . uniqid('', false); + + if (!$extracted) { + $extracted = self::unZip($zip, $tmp); + if (!$extracted) { + Folder::delete($tmp); + return false; + } + } + + if (!file_exists($extracted)) { + self::$error = self::INVALID_SOURCE; + return false; + } + + $is_install = true; + $installer = self::loadInstaller($extracted, $is_install); + + if (isset($options['is_update']) && $options['is_update'] === true) { + $method = 'preUpdate'; + } else { + $method = 'preInstall'; + } + + if ($installer && method_exists($installer, $method)) { + $method_result = $installer::$method(); + if ($method_result !== true) { + self::$error = 'An error occurred'; + if (is_string($method_result)) { + self::$error = $method_result; + } + + return false; + } + } + + if (!$options['sophisticated']) { + $isTheme = $options['theme'] ?? false; + // Make sure that themes are always being copied, even if option was not set! + $isTheme = $isTheme || preg_match('|/themes/[^/]+|ui', $install_path); + if ($isTheme) { + self::copyInstall($extracted, $install_path); + } else { + self::moveInstall($extracted, $install_path); + } + } else { + self::sophisticatedInstall($extracted, $install_path, $options['ignores'], $keepExtracted); + } + + Folder::delete($tmp); + + if (isset($options['is_update']) && $options['is_update'] === true) { + $method = 'postUpdate'; + } else { + $method = 'postInstall'; + } + + self::$message = ''; + if ($installer && method_exists($installer, $method)) { + self::$message = $installer::$method(); + } + + self::$error = self::OK; + + return true; + } + + /** + * Unzip a file to somewhere + * + * @param string $zip_file + * @param string $destination + * @return string|false + */ + public static function unZip($zip_file, $destination) + { + $zip = new ZipArchive(); + $archive = $zip->open($zip_file); + + if ($archive === true) { + Folder::create($destination); + + $unzip = $zip->extractTo($destination); + + + if (!$unzip) { + self::$error = self::ZIP_EXTRACT_ERROR; + Folder::delete($destination); + $zip->close(); + return false; + } + + $package_folder_name = $zip->getNameIndex(0); + if ($package_folder_name === false) { + throw new \RuntimeException('Bad package file: ' . basename($zip_file)); + } + $package_folder_name = preg_replace('#\./$#', '', $package_folder_name); + $zip->close(); + + return $destination . '/' . $package_folder_name; + } + + self::$error = self::ZIP_EXTRACT_ERROR; + self::$error_zip = $archive; + + return false; + } + + /** + * Instantiates and returns the package installer class + * + * @param string $installer_file_folder The folder path that contains install.php + * @param bool $is_install True if install, false if removal + * @return string|null + */ + private static function loadInstaller($installer_file_folder, $is_install) + { + $installer_file_folder = rtrim($installer_file_folder, DS); + + $install_file = $installer_file_folder . DS . 'install.php'; + + if (!file_exists($install_file)) { + return null; + } + + require_once $install_file; + + if ($is_install) { + $slug = ''; + if (($pos = strpos($installer_file_folder, 'grav-plugin-')) !== false) { + $slug = substr($installer_file_folder, $pos + strlen('grav-plugin-')); + } elseif (($pos = strpos($installer_file_folder, 'grav-theme-')) !== false) { + $slug = substr($installer_file_folder, $pos + strlen('grav-theme-')); + } + } else { + $path_elements = explode('/', $installer_file_folder); + $slug = end($path_elements); + } + + if (!$slug) { + return null; + } + + $class_name = ucfirst($slug) . 'Install'; + + if (class_exists($class_name)) { + return $class_name; + } + + $class_name_alphanumeric = preg_replace('/[^a-zA-Z0-9]+/', '', $class_name) ?? $class_name; + + if (class_exists($class_name_alphanumeric)) { + return $class_name_alphanumeric; + } + + return null; + } + + /** + * @param string $source_path + * @param string $install_path + * @return bool + */ + public static function moveInstall($source_path, $install_path) + { + if (file_exists($install_path)) { + Folder::delete($install_path); + } + + Folder::move($source_path, $install_path); + + return true; + } + + /** + * @param string $source_path + * @param string $install_path + * @return bool + */ + public static function copyInstall($source_path, $install_path) + { + if (empty($source_path)) { + throw new RuntimeException("Directory $source_path is missing"); + } + + Folder::rcopy($source_path, $install_path); + + return true; + } + + /** + * @param string $source_path + * @param string $install_path + * @param array $ignores + * @param bool $keep_source + * @return bool + */ + public static function sophisticatedInstall($source_path, $install_path, $ignores = [], $keep_source = false) + { + foreach (new DirectoryIterator($source_path) as $file) { + if ($file->isLink() || $file->isDot() || in_array($file->getFilename(), $ignores, true)) { + continue; + } + + $path = $install_path . DS . $file->getFilename(); + + if ($file->isDir()) { + Folder::delete($path); + if ($keep_source) { + Folder::copy($file->getPathname(), $path); + } else { + Folder::move($file->getPathname(), $path); + } + + if ($file->getFilename() === 'bin') { + $glob = glob($path . DS . '*') ?: []; + foreach ($glob as $bin_file) { + @chmod($bin_file, 0755); + } + } + } else { + @unlink($path); + @copy($file->getPathname(), $path); + } + } + + return true; + } + + /** + * Uninstalls one or more given package + * + * @param string $path The slug of the package(s) + * @param array $options Options to use for uninstalling + * @return bool True if everything went fine, False otherwise. + */ + public static function uninstall($path, $options = []) + { + $options = array_merge(self::$options, $options); + if (!self::isValidDestination($path, $options['exclude_checks']) + ) { + return false; + } + + $installer_file_folder = $path; + $is_install = false; + $installer = self::loadInstaller($installer_file_folder, $is_install); + + if ($installer && method_exists($installer, 'preUninstall')) { + $method_result = $installer::preUninstall(); + if ($method_result !== true) { + self::$error = 'An error occurred'; + if (is_string($method_result)) { + self::$error = $method_result; + } + + return false; + } + } + + $result = Folder::delete($path); + + self::$message = ''; + if ($result && $installer && method_exists($installer, 'postUninstall')) { + self::$message = $installer::postUninstall(); + } + + return $result; + } + + /** + * Runs a set of checks on the destination and sets the Error if any + * + * @param string $destination The directory to run validations at + * @param array $exclude An array of constants to exclude from the validation + * @return bool True if validation passed. False otherwise + */ + public static function isValidDestination($destination, $exclude = []) + { + self::$error = 0; + self::$target = $destination; + + if (is_link($destination)) { + self::$error = self::IS_LINK; + } elseif (file_exists($destination)) { + self::$error = self::EXISTS; + } elseif (!file_exists($destination)) { + self::$error = self::NOT_FOUND; + } elseif (!is_dir($destination)) { + self::$error = self::NOT_DIRECTORY; + } + + if (count($exclude) && in_array(self::$error, $exclude, true)) { + return true; + } + + return !self::$error; + } + + /** + * Validates if the given path is a Grav Instance + * + * @param string $target The local path to the Grav Instance + * @return bool True if is a Grav Instance. False otherwise + */ + public static function isGravInstance($target) + { + self::$error = 0; + self::$target = $target; + + if (!file_exists($target . DS . 'index.php') || + !file_exists($target . DS . 'bin') || + !file_exists($target . DS . 'user') || + !file_exists($target . DS . 'system' . DS . 'config' . DS . 'system.yaml') + ) { + self::$error = self::NOT_GRAV_ROOT; + } + + return !self::$error; + } + + /** + * Returns the last message added by the installer + * + * @return string The message + */ + public static function getMessage() + { + return self::$message; + } + + /** + * Returns the last error occurred in a string message format + * + * @return string The message of the last error + */ + public static function lastErrorMsg() + { + if (is_string(self::$error)) { + return self::$error; + } + + switch (self::$error) { + case 0: + $msg = 'No Error'; + break; + + case self::EXISTS: + $msg = 'The target path "' . self::$target . '" already exists'; + break; + + case self::IS_LINK: + $msg = 'The target path "' . self::$target . '" is a symbolic link'; + break; + + case self::NOT_FOUND: + $msg = 'The target path "' . self::$target . '" does not appear to exist'; + break; + + case self::NOT_DIRECTORY: + $msg = 'The target path "' . self::$target . '" does not appear to be a folder'; + break; + + case self::NOT_GRAV_ROOT: + $msg = 'The target path "' . self::$target . '" does not appear to be a Grav instance'; + break; + + case self::ZIP_OPEN_ERROR: + $msg = 'Unable to open the package file'; + break; + + case self::ZIP_EXTRACT_ERROR: + $msg = 'Unable to extract the package. '; + if (self::$error_zip) { + switch (self::$error_zip) { + case ZipArchive::ER_EXISTS: + $msg .= 'File already exists.'; + break; + + case ZipArchive::ER_INCONS: + $msg .= 'Zip archive inconsistent.'; + break; + + case ZipArchive::ER_MEMORY: + $msg .= 'Memory allocation failure.'; + break; + + case ZipArchive::ER_NOENT: + $msg .= 'No such file.'; + break; + + case ZipArchive::ER_NOZIP: + $msg .= 'Not a zip archive.'; + break; + + case ZipArchive::ER_OPEN: + $msg .= "Can't open file."; + break; + + case ZipArchive::ER_READ: + $msg .= 'Read error.'; + break; + + case ZipArchive::ER_SEEK: + $msg .= 'Seek error.'; + break; + } + } + break; + + case self::INVALID_SOURCE: + $msg = 'Invalid source file'; + break; + + default: + $msg = 'Unknown Error'; + break; + } + + return $msg; + } + + /** + * Returns the last error code of the occurred error + * + * @return int|string The code of the last error + */ + public static function lastErrorCode() + { + return self::$error; + } + + /** + * Allows to manually set an error + * + * @param int|string $error the Error code + * @return void + */ + public static function setError($error) + { + self::$error = $error; + } +} diff --git a/system/src/Grav/Common/GPM/Licenses.php b/system/src/Grav/Common/GPM/Licenses.php new file mode 100644 index 0000000..825343d --- /dev/null +++ b/system/src/Grav/Common/GPM/Licenses.php @@ -0,0 +1,116 @@ +content(); + $slug = strtolower($slug); + + if ($license && !self::validate($license)) { + return false; + } + + if (!is_string($license)) { + if (isset($data['licenses'][$slug])) { + unset($data['licenses'][$slug]); + } else { + return false; + } + } else { + $data['licenses'][$slug] = $license; + } + + $licenses->save($data); + $licenses->free(); + + return true; + } + + /** + * Returns the license for a Premium package + * + * @param string|null $slug + * @return string[]|string + */ + public static function get($slug = null) + { + $licenses = self::getLicenseFile(); + $data = (array)$licenses->content(); + $licenses->free(); + + if (null === $slug) { + return $data['licenses'] ?? []; + } + + $slug = strtolower($slug); + + return $data['licenses'][$slug] ?? ''; + } + + + /** + * Validates the License format + * + * @param string|null $license + * @return bool + */ + public static function validate($license = null) + { + if (!is_string($license)) { + return false; + } + + return (bool)preg_match('#' . self::$regex. '#', $license); + } + + /** + * Get the License File object + * + * @return FileInterface + */ + public static function getLicenseFile() + { + if (!isset(self::$file)) { + $path = Grav::instance()['locator']->findResource('user-data://') . '/licenses.yaml'; + if (!file_exists($path)) { + touch($path); + } + self::$file = CompiledYamlFile::instance($path); + } + + return self::$file; + } +} diff --git a/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php new file mode 100644 index 0000000..d1f3e46 --- /dev/null +++ b/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php @@ -0,0 +1,34 @@ + $data) { + $data->set('slug', $name); + $this->items[$name] = new Package($data, $this->type); + } + } +} diff --git a/system/src/Grav/Common/GPM/Local/Package.php b/system/src/Grav/Common/GPM/Local/Package.php new file mode 100644 index 0000000..ffe2c63 --- /dev/null +++ b/system/src/Grav/Common/GPM/Local/Package.php @@ -0,0 +1,51 @@ +blueprints()->toArray()); + parent::__construct($data, $package_type); + + $this->settings = $package->toArray(); + + $html_description = Parsedown::instance()->line($this->__get('description')); + $this->data->set('slug', $package->__get('slug')); + $this->data->set('description_html', $html_description); + $this->data->set('description_plain', strip_tags($html_description)); + $this->data->set('symlink', is_link(USER_DIR . $package_type . DS . $this->__get('slug'))); + } + + /** + * @return bool + */ + public function isEnabled() + { + return (bool)$this->settings['enabled']; + } +} diff --git a/system/src/Grav/Common/GPM/Local/Packages.php b/system/src/Grav/Common/GPM/Local/Packages.php new file mode 100644 index 0000000..0290296 --- /dev/null +++ b/system/src/Grav/Common/GPM/Local/Packages.php @@ -0,0 +1,29 @@ + new Plugins(), + 'themes' => new Themes() + ]; + + parent::__construct($items); + } +} diff --git a/system/src/Grav/Common/GPM/Local/Plugins.php b/system/src/Grav/Common/GPM/Local/Plugins.php new file mode 100644 index 0000000..8a6574b --- /dev/null +++ b/system/src/Grav/Common/GPM/Local/Plugins.php @@ -0,0 +1,33 @@ +all()); + } +} diff --git a/system/src/Grav/Common/GPM/Local/Themes.php b/system/src/Grav/Common/GPM/Local/Themes.php new file mode 100644 index 0000000..73253cc --- /dev/null +++ b/system/src/Grav/Common/GPM/Local/Themes.php @@ -0,0 +1,33 @@ +all()); + } +} diff --git a/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php new file mode 100644 index 0000000..9e68d66 --- /dev/null +++ b/system/src/Grav/Common/GPM/Remote/AbstractPackageCollection.php @@ -0,0 +1,81 @@ +get('system.gpm.releases', 'stable'); + $cache_dir = Grav::instance()['locator']->findResource('cache://gpm', true, true); + $this->cache = new FilesystemCache($cache_dir); + + $this->repository = $repository . '?v=' . GRAV_VERSION . '&' . $channel . '=1'; + $this->raw = $this->cache->fetch(md5($this->repository)); + + $this->fetch($refresh, $callback); + foreach (json_decode($this->raw, true) as $slug => $data) { + // Temporarily fix for using multi-sites + if (isset($data['install_path'])) { + $path = preg_replace('~^user/~i', 'user://', $data['install_path']); + $data['install_path'] = Grav::instance()['locator']->findResource($path, false, true); + } + $this->items[$slug] = new Package($data, $this->type); + } + } + + /** + * @param bool $refresh + * @param callable|null $callback + * @return string + */ + public function fetch($refresh = false, $callback = null) + { + if (!$this->raw || $refresh) { + $response = Response::get($this->repository, [], $callback); + $this->raw = $response; + $this->cache->save(md5($this->repository), $this->raw, $this->lifetime); + } + + return $this->raw; + } +} diff --git a/system/src/Grav/Common/GPM/Remote/GravCore.php b/system/src/Grav/Common/GPM/Remote/GravCore.php new file mode 100644 index 0000000..f93209e --- /dev/null +++ b/system/src/Grav/Common/GPM/Remote/GravCore.php @@ -0,0 +1,151 @@ +get('system.gpm.releases', 'stable'); + $cache_dir = Grav::instance()['locator']->findResource('cache://gpm', true, true); + $this->cache = new FilesystemCache($cache_dir); + $this->repository .= '?v=' . GRAV_VERSION . '&' . $channel . '=1'; + $this->raw = $this->cache->fetch(md5($this->repository)); + + $this->fetch($refresh, $callback); + + $this->data = json_decode($this->raw, true); + $this->version = $this->data['version'] ?? '-'; + $this->date = $this->data['date'] ?? '-'; + $this->min_php = $this->data['min_php'] ?? null; + + if (isset($this->data['assets'])) { + foreach ((array)$this->data['assets'] as $slug => $data) { + $this->items[$slug] = new Package($data); + } + } + } + + /** + * Returns the list of assets associated to the latest version of Grav + * + * @return array list of assets + */ + public function getAssets() + { + return $this->data['assets']; + } + + /** + * Returns the changelog list for each version of Grav + * + * @param string|null $diff the version number to start the diff from + * @return array changelog list for each version + */ + public function getChangelog($diff = null) + { + if (!$diff) { + return $this->data['changelog']; + } + + $diffLog = []; + foreach ((array)$this->data['changelog'] as $version => $changelog) { + preg_match("/[\w\-\.]+/", $version, $cleanVersion); + + if (!$cleanVersion || version_compare($diff, $cleanVersion[0], '>=')) { + continue; + } + + $diffLog[$version] = $changelog; + } + + return $diffLog; + } + + /** + * Return the release date of the latest Grav + * + * @return string + */ + public function getDate() + { + return $this->date; + } + + /** + * Determine if this version of Grav is eligible to be updated + * + * @return mixed + */ + public function isUpdatable() + { + return version_compare(GRAV_VERSION, $this->getVersion(), '<'); + } + + /** + * Returns the latest version of Grav available remotely + * + * @return string + */ + public function getVersion() + { + return $this->version; + } + + /** + * Returns the minimum PHP version + * + * @return string + */ + public function getMinPHPVersion() + { + // If non min set, assume current PHP version + if (null === $this->min_php) { + $this->min_php = PHP_VERSION; + } + + return $this->min_php; + } + + /** + * Is this installation symlinked? + * + * @return bool + */ + public function isSymlink() + { + return is_link(GRAV_ROOT . DS . 'index.php'); + } +} diff --git a/system/src/Grav/Common/GPM/Remote/Package.php b/system/src/Grav/Common/GPM/Remote/Package.php new file mode 100644 index 0000000..c37d6a0 --- /dev/null +++ b/system/src/Grav/Common/GPM/Remote/Package.php @@ -0,0 +1,65 @@ +data->toArray(); + } + + /** + * Returns the changelog list for each version of a package + * + * @param string|null $diff the version number to start the diff from + * @return array changelog list for each version + */ + public function getChangelog($diff = null) + { + if (!$diff) { + return $this->data['changelog']; + } + + $diffLog = []; + foreach ((array)$this->data['changelog'] as $version => $changelog) { + preg_match("/[\w\-.]+/", $version, $cleanVersion); + + if (!$cleanVersion || version_compare($diff, $cleanVersion[0], '>=')) { + continue; + } + + $diffLog[$version] = $changelog; + } + + return $diffLog; + } +} diff --git a/system/src/Grav/Common/GPM/Remote/Packages.php b/system/src/Grav/Common/GPM/Remote/Packages.php new file mode 100644 index 0000000..f55c12d --- /dev/null +++ b/system/src/Grav/Common/GPM/Remote/Packages.php @@ -0,0 +1,34 @@ + new Plugins($refresh, $callback), + 'themes' => new Themes($refresh, $callback) + ]; + + parent::__construct($items); + } +} diff --git a/system/src/Grav/Common/GPM/Remote/Plugins.php b/system/src/Grav/Common/GPM/Remote/Plugins.php new file mode 100644 index 0000000..a134a10 --- /dev/null +++ b/system/src/Grav/Common/GPM/Remote/Plugins.php @@ -0,0 +1,32 @@ +repository, $refresh, $callback); + } +} diff --git a/system/src/Grav/Common/GPM/Remote/Themes.php b/system/src/Grav/Common/GPM/Remote/Themes.php new file mode 100644 index 0000000..160ac97 --- /dev/null +++ b/system/src/Grav/Common/GPM/Remote/Themes.php @@ -0,0 +1,32 @@ +repository, $refresh, $callback); + } +} diff --git a/system/src/Grav/Common/GPM/Response.php b/system/src/Grav/Common/GPM/Response.php new file mode 100644 index 0000000..98654b6 --- /dev/null +++ b/system/src/Grav/Common/GPM/Response.php @@ -0,0 +1,3 @@ +remote = new Remote\GravCore($refresh, $callback); + } + + /** + * Returns the release date of the latest version of Grav + * + * @return string + */ + public function getReleaseDate() + { + return $this->remote->getDate(); + } + + /** + * Returns the version of the installed Grav + * + * @return string + */ + public function getLocalVersion() + { + return GRAV_VERSION; + } + + /** + * Returns the version of the remotely available Grav + * + * @return string + */ + public function getRemoteVersion() + { + return $this->remote->getVersion(); + } + + /** + * Returns an array of assets available to download remotely + * + * @return array + */ + public function getAssets() + { + return $this->remote->getAssets(); + } + + /** + * Returns the changelog list for each version of Grav + * + * @param string|null $diff the version number to start the diff from + * @return array return the changelog list for each version + */ + public function getChangelog($diff = null) + { + return $this->remote->getChangelog($diff); + } + + /** + * Make sure this meets minimum PHP requirements + * + * @return bool + */ + public function meetsRequirements() + { + if (version_compare(PHP_VERSION, $this->minPHPVersion(), '<')) { + return false; + } + + return true; + } + + /** + * Get minimum PHP version from remote + * + * @return string + */ + public function minPHPVersion() + { + if (null === $this->min_php) { + $this->min_php = $this->remote->getMinPHPVersion(); + } + + return $this->min_php; + } + + /** + * Checks if the currently installed Grav is upgradable to a newer version + * + * @return bool True if it's upgradable, False otherwise. + */ + public function isUpgradable() + { + return version_compare($this->getLocalVersion(), $this->getRemoteVersion(), '<'); + } + + /** + * Checks if Grav is currently symbolically linked + * + * @return bool True if Grav is symlinked, False otherwise. + */ + public function isSymlink() + { + return $this->remote->isSymlink(); + } +} diff --git a/system/src/Grav/Common/Getters.php b/system/src/Grav/Common/Getters.php new file mode 100644 index 0000000..916d524 --- /dev/null +++ b/system/src/Grav/Common/Getters.php @@ -0,0 +1,165 @@ +offsetSet($offset, $value); + } + + /** + * Magic getter method + * + * @param int|string $offset Medium name value + * @return mixed Medium value + */ + public function __get($offset) + { + return $this->offsetGet($offset); + } + + /** + * Magic method to determine if the attribute is set + * + * @param int|string $offset Medium name value + * @return boolean True if the value is set + */ + public function __isset($offset) + { + return $this->offsetExists($offset); + } + + /** + * Magic method to unset the attribute + * + * @param int|string $offset The name value to unset + */ + public function __unset($offset) + { + $this->offsetUnset($offset); + } + + /** + * @param int|string $offset + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + if ($this->gettersVariable) { + $var = $this->gettersVariable; + + return isset($this->{$var}[$offset]); + } + + return isset($this->{$offset}); + } + + /** + * @param int|string $offset + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + if ($this->gettersVariable) { + $var = $this->gettersVariable; + + return $this->{$var}[$offset] ?? null; + } + + return $this->{$offset} ?? null; + } + + /** + * @param int|string $offset + * @param mixed $value + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + if ($this->gettersVariable) { + $var = $this->gettersVariable; + $this->{$var}[$offset] = $value; + } else { + $this->{$offset} = $value; + } + } + + /** + * @param int|string $offset + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + if ($this->gettersVariable) { + $var = $this->gettersVariable; + unset($this->{$var}[$offset]); + } else { + unset($this->{$offset}); + } + } + + /** + * @return int + */ + public function count() + { + if ($this->gettersVariable) { + $var = $this->gettersVariable; + return count($this->{$var}); + } + + return count($this->toArray()); + } + + /** + * Returns an associative array of object properties. + * + * @return array + */ + public function toArray() + { + if ($this->gettersVariable) { + $var = $this->gettersVariable; + + return $this->{$var}; + } + + $properties = (array)$this; + $list = []; + foreach ($properties as $property => $value) { + if ($property[0] !== "\0") { + $list[$property] = $value; + } + } + + return $list; + } +} diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php new file mode 100644 index 0000000..2398c11 --- /dev/null +++ b/system/src/Grav/Common/Grav.php @@ -0,0 +1,786 @@ + Browser::class, + 'cache' => Cache::class, + 'events' => EventDispatcher::class, + 'exif' => Exif::class, + 'plugins' => Plugins::class, + 'scheduler' => Scheduler::class, + 'taxonomy' => Taxonomy::class, + 'themes' => Themes::class, + 'twig' => Twig::class, + 'uri' => Uri::class, + ]; + + /** + * @var array All middleware processors that are processed in $this->process() + */ + protected $middleware = [ + 'initializeProcessor', + 'pluginsProcessor', + 'themesProcessor', + 'requestProcessor', + 'tasksProcessor', + 'backupsProcessor', + 'schedulerProcessor', + 'assetsProcessor', + 'twigProcessor', + 'pagesProcessor', + 'debuggerAssetsProcessor', + 'renderProcessor', + ]; + + /** @var array */ + protected $initialized = []; + + /** + * Reset the Grav instance. + * + * @return void + */ + public static function resetInstance(): void + { + if (self::$instance) { + // @phpstan-ignore-next-line + self::$instance = null; + } + } + + /** + * Return the Grav instance. Create it if it's not already instanced + * + * @param array $values + * @return Grav + */ + public static function instance(array $values = []) + { + if (null === self::$instance) { + self::$instance = static::load($values); + + /** @var ClassLoader|null $loader */ + $loader = self::$instance['loader'] ?? null; + if ($loader) { + // Load fix for Deferred Twig Extension + $loader->addPsr4('Phive\\Twig\\Extensions\\Deferred\\', LIB_DIR . 'Phive/Twig/Extensions/Deferred/', true); + } + } elseif ($values) { + $instance = self::$instance; + foreach ($values as $key => $value) { + $instance->offsetSet($key, $value); + } + } + + return self::$instance; + } + + /** + * Get Grav version. + * + * @return string + */ + public function getVersion(): string + { + return GRAV_VERSION; + } + + /** + * @return bool + */ + public function isSetup(): bool + { + return isset($this->initialized['setup']); + } + + /** + * Setup Grav instance using specific environment. + * + * @param string|null $environment + * @return $this + */ + public function setup(string $environment = null) + { + if (isset($this->initialized['setup'])) { + return $this; + } + + $this->initialized['setup'] = true; + + // Force environment if passed to the method. + if ($environment) { + Setup::$environment = $environment; + } + + // Initialize setup and streams. + $this['setup']; + $this['streams']; + + return $this; + } + + /** + * Initialize CLI environment. + * + * Call after `$grav->setup($environment)` + * + * - Load configuration + * - Initialize logger + * - Disable debugger + * - Set timezone, locale + * - Load plugins (call PluginsLoadedEvent) + * - Set Pages and Users type to be used in the site + * + * This method WILL NOT initialize assets, twig or pages. + * + * @return $this + */ + public function initializeCli() + { + InitializeProcessor::initializeCli($this); + + return $this; + } + + /** + * Process a request + * + * @return void + */ + public function process(): void + { + if (isset($this->initialized['process'])) { + return; + } + + // Initialize Grav if needed. + $this->setup(); + + $this->initialized['process'] = true; + + $container = new Container( + [ + 'initializeProcessor' => function () { + return new InitializeProcessor($this); + }, + 'backupsProcessor' => function () { + return new BackupsProcessor($this); + }, + 'pluginsProcessor' => function () { + return new PluginsProcessor($this); + }, + 'themesProcessor' => function () { + return new ThemesProcessor($this); + }, + 'schedulerProcessor' => function () { + return new SchedulerProcessor($this); + }, + 'requestProcessor' => function () { + return new RequestProcessor($this); + }, + 'tasksProcessor' => function () { + return new TasksProcessor($this); + }, + 'assetsProcessor' => function () { + return new AssetsProcessor($this); + }, + 'twigProcessor' => function () { + return new TwigProcessor($this); + }, + 'pagesProcessor' => function () { + return new PagesProcessor($this); + }, + 'debuggerAssetsProcessor' => function () { + return new DebuggerAssetsProcessor($this); + }, + 'renderProcessor' => function () { + return new RenderProcessor($this); + }, + ] + ); + + $default = static function () { + return new Response(404, ['Expires' => 0, 'Cache-Control' => 'no-store, max-age=0'], 'Not Found'); + }; + + $collection = new RequestHandler($this->middleware, $default, $container); + + $response = $collection->handle($this['request']); + $body = $response->getBody(); + + /** @var Messages $messages */ + $messages = $this['messages']; + + // Prevent caching if session messages were displayed in the page. + $noCache = $messages->isCleared(); + if ($noCache) { + $response = $response->withHeader('Cache-Control', 'no-store, max-age=0'); + } + + // Handle ETag and If-None-Match headers. + if ($response->getHeaderLine('ETag') === '1') { + $etag = md5($body); + $response = $response->withHeader('ETag', '"' . $etag . '"'); + + $search = trim($this['request']->getHeaderLine('If-None-Match'), '"'); + if ($noCache === false && $search === $etag) { + $response = $response->withStatus(304); + $body = ''; + } + } + + // Echo page content. + $this->header($response); + echo $body; + + $this['debugger']->render(); + + // Response object can turn off all shutdown processing. This can be used for example to speed up AJAX responses. + // Note that using this feature will also turn off response compression. + if ($response->getHeaderLine('Grav-Internal-SkipShutdown') !== '1') { + register_shutdown_function([$this, 'shutdown']); + } + } + + /** + * Terminates Grav request with a response. + * + * Please use this method instead of calling `die();` or `exit();`. Note that you need to create a response object. + * + * @param ResponseInterface $response + * @return void + */ + public function close(ResponseInterface $response): void + { + // Make sure nothing extra gets written to the response. + while (ob_get_level()) { + ob_end_clean(); + } + + // Close the session. + if (isset($this['session'])) { + $this['session']->close(); + } + + /** @var ServerRequestInterface $request */ + $request = $this['request']; + + /** @var Debugger $debugger */ + $debugger = $this['debugger']; + $response = $debugger->logRequest($request, $response); + + $body = $response->getBody(); + + /** @var Messages $messages */ + $messages = $this['messages']; + + // Prevent caching if session messages were displayed in the page. + $noCache = $messages->isCleared(); + if ($noCache) { + $response = $response->withHeader('Cache-Control', 'no-store, max-age=0'); + } + + // Handle ETag and If-None-Match headers. + if ($response->getHeaderLine('ETag') === '1') { + $etag = md5($body); + $response = $response->withHeader('ETag', '"' . $etag . '"'); + + $search = trim($this['request']->getHeaderLine('If-None-Match'), '"'); + if ($noCache === false && $search === $etag) { + $response = $response->withStatus(304); + $body = ''; + } + } + + // Echo page content. + $this->header($response); + echo $body; + exit(); + } + + /** + * @param ResponseInterface $response + * @return void + * @deprecated 1.7 Do not use + */ + public function exit(ResponseInterface $response): void + { + $this->close($response); + } + + /** + * Terminates Grav request and redirects browser to another location. + * + * Please use this method instead of calling `header("Location: {$url}", true, 302); exit();`. + * + * @param string $route Internal route. + * @param int|null $code Redirection code (30x) + * @return void + */ + public function redirect($route, $code = null): void + { + $response = $this->getRedirectResponse($route, $code); + + $this->close($response); + } + + /** + * Returns redirect response object from Grav. + * + * @param string $route Internal route. + * @param int|null $code Redirection code (30x) + * @return ResponseInterface + */ + public function getRedirectResponse($route, $code = null): ResponseInterface + { + /** @var Uri $uri */ + $uri = $this['uri']; + + // Clean route for redirect + $route = preg_replace("#^\/[\\\/]+\/#", '/', $route); + + if ($code < 300 || $code > 399) { + $code = null; + } + + if (null === $code) { + // Check for redirect code in the route: e.g. /new/[301], /new[301]/route or /new[301].html + $regex = '/.*(\[(30[1-7])\])(.\w+|\/.*?)?$/'; + preg_match($regex, $route, $matches); + if ($matches) { + $route = str_replace($matches[1], '', $matches[0]); + $code = $matches[2]; + } + } + + if ($code === null) { + $code = $this['config']->get('system.pages.redirect_default_code', 302); + } + + if ($uri::isExternal($route)) { + $url = $route; + } else { + $url = rtrim($uri->rootUrl(), '/') . '/'; + + if ($this['config']->get('system.pages.redirect_trailing_slash', true)) { + $url .= trim($route, '/'); // Remove trailing slash + } else { + $url .= ltrim($route, '/'); // Support trailing slash default routes + } + } + + if ($uri->extension() === 'json') { + return new Response(200, ['Content-Type' => 'application/json'], json_encode(['code' => $code, 'redirect' => $url], JSON_THROW_ON_ERROR)); + } + + return new Response($code, ['Location' => $url]); + } + + /** + * Redirect browser to another location taking language into account (preferred) + * + * @param string $route Internal route. + * @param int $code Redirection code (30x) + * @return void + */ + public function redirectLangSafe($route, $code = null): void + { + if (!$this['uri']->isExternal($route)) { + $this->redirect($this['pages']->route($route), $code); + } else { + $this->redirect($route, $code); + } + } + + /** + * Set response header. + * + * @param ResponseInterface|null $response + * @return void + */ + public function header(ResponseInterface $response = null): void + { + if (null === $response) { + /** @var PageInterface $page */ + $page = $this['page']; + $response = new Response($page->httpResponseCode(), $page->httpHeaders(), ''); + } + + header("HTTP/{$response->getProtocolVersion()} {$response->getStatusCode()} {$response->getReasonPhrase()}"); + foreach ($response->getHeaders() as $key => $values) { + // Skip internal Grav headers. + if (strpos($key, 'Grav-Internal-') === 0) { + continue; + } + foreach ($values as $i => $value) { + header($key . ': ' . $value, $i === 0); + } + } + } + + /** + * Set the system locale based on the language and configuration + * + * @return void + */ + public function setLocale(): void + { + // Initialize Locale if set and configured. + if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) { + $language = $this['language']->getLanguage(); + setlocale(LC_ALL, strlen($language) < 3 ? ($language . '_' . strtoupper($language)) : $language); + } elseif ($this['config']->get('system.default_locale')) { + setlocale(LC_ALL, $this['config']->get('system.default_locale')); + } + } + + /** + * @param object $event + * @return object + */ + public function dispatchEvent($event) + { + /** @var EventDispatcherInterface $events */ + $events = $this['events']; + $eventName = get_class($event); + + $timestamp = microtime(true); + $event = $events->dispatch($event); + + /** @var Debugger $debugger */ + $debugger = $this['debugger']; + $debugger->addEvent($eventName, $event, $events, $timestamp); + + return $event; + } + + /** + * Fires an event with optional parameters. + * + * @param string $eventName + * @param Event|null $event + * @return Event + */ + public function fireEvent($eventName, Event $event = null) + { + /** @var EventDispatcherInterface $events */ + $events = $this['events']; + if (null === $event) { + $event = new Event(); + } + + $timestamp = microtime(true); + $events->dispatch($event, $eventName); + + /** @var Debugger $debugger */ + $debugger = $this['debugger']; + $debugger->addEvent($eventName, $event, $events, $timestamp); + + return $event; + } + + /** + * Set the final content length for the page and flush the buffer + * + * @return void + */ + public function shutdown(): void + { + // Prevent user abort allowing onShutdown event to run without interruptions. + if (function_exists('ignore_user_abort')) { + @ignore_user_abort(true); + } + + // Close the session allowing new requests to be handled. + if (isset($this['session'])) { + $this['session']->close(); + } + + /** @var Config $config */ + $config = $this['config']; + if ($config->get('system.debugger.shutdown.close_connection', true)) { + // Flush the response and close the connection to allow time consuming tasks to be performed without leaving + // the connection to the client open. This will make page loads to feel much faster. + + // FastCGI allows us to flush all response data to the client and finish the request. + $success = function_exists('fastcgi_finish_request') ? @fastcgi_finish_request() : false; + if (!$success) { + // Unfortunately without FastCGI there is no way to force close the connection. + // We need to ask browser to close the connection for us. + + if ($config->get('system.cache.gzip')) { + // Flush gzhandler buffer if gzip setting was enabled to get the size of the compressed output. + ob_end_flush(); + } elseif ($config->get('system.cache.allow_webserver_gzip')) { + // Let web server to do the hard work. + header('Content-Encoding: identity'); + } elseif (function_exists('apache_setenv')) { + // Without gzip we have no other choice than to prevent server from compressing the output. + // This action turns off mod_deflate which would prevent us from closing the connection. + @apache_setenv('no-gzip', '1'); + } else { + // Fall back to unknown content encoding, it prevents most servers from deflating the content. + header('Content-Encoding: none'); + } + + // Get length and close the connection. + header('Content-Length: ' . ob_get_length()); + header('Connection: close'); + + ob_end_flush(); + @ob_flush(); + flush(); + } + } + + // Run any time consuming tasks. + $this->fireEvent('onShutdown'); + } + + /** + * Magic Catch All Function + * + * Used to call closures. + * + * Source: http://stackoverflow.com/questions/419804/closures-as-class-members + * + * @param string $method + * @param array $args + * @return mixed|null + */ + public function __call($method, $args) + { + $closure = $this->{$method} ?? null; + + return is_callable($closure) ? $closure(...$args) : null; + } + + /** + * Measure how long it takes to do an action. + * + * @param string $timerId + * @param string $timerTitle + * @param callable $callback + * @return mixed Returns value returned by the callable. + */ + public function measureTime(string $timerId, string $timerTitle, callable $callback) + { + $debugger = $this['debugger']; + $debugger->startTimer($timerId, $timerTitle); + $result = $callback(); + $debugger->stopTimer($timerId); + + return $result; + } + + /** + * Initialize and return a Grav instance + * + * @param array $values + * @return static + */ + protected static function load(array $values) + { + $container = new static($values); + + $container['debugger'] = new Debugger(); + $container['grav'] = function (Container $container) { + user_error('Calling $grav[\'grav\'] or {{ grav.grav }} is deprecated since Grav 1.6, just use $grav or {{ grav }}', E_USER_DEPRECATED); + + return $container; + }; + + $container->registerServices(); + + return $container; + } + + /** + * Register all services + * Services are defined in the diMap. They can either only the class + * of a Service Provider or a pair of serviceKey => serviceClass that + * gets directly mapped into the container. + * + * @return void + */ + protected function registerServices(): void + { + foreach (self::$diMap as $serviceKey => $serviceClass) { + if (is_int($serviceKey)) { + $this->register(new $serviceClass); + } else { + $this[$serviceKey] = function ($c) use ($serviceClass) { + return new $serviceClass($c); + }; + } + } + } + + /** + * This attempts to find media, other files, and download them + * + * @param string $path + * @return PageInterface|false + */ + public function fallbackUrl($path) + { + $this->fireEvent('onPageFallBackUrl'); + + /** @var Uri $uri */ + $uri = $this['uri']; + + /** @var Config $config */ + $config = $this['config']; + + $uri_extension = strtolower($uri->extension()); + $fallback_types = $config->get('system.media.allowed_fallback_types', null); + $supported_types = $config->get('media.types'); + + // Check whitelist first, then ensure extension is a valid media type + if (!empty($fallback_types) && !in_array($uri_extension, $fallback_types, true)) { + return false; + } + if (!array_key_exists($uri_extension, $supported_types)) { + return false; + } + + $path_parts = pathinfo($path); + + /** @var Pages $pages */ + $pages = $this['pages']; + $page = $pages->find($path_parts['dirname'], true); + + if ($page) { + $media = $page->media()->all(); + $parsed_url = parse_url(rawurldecode($uri->basename())); + $media_file = $parsed_url['path']; + + // if this is a media object, try actions first + if (isset($media[$media_file])) { + /** @var Medium $medium */ + $medium = $media[$media_file]; + foreach ($uri->query(null, true) as $action => $params) { + if (in_array($action, ImageMedium::$magic_actions, true)) { + call_user_func_array([&$medium, $action], explode(',', $params)); + } + } + Utils::download($medium->path(), false); + } + + // unsupported media type, try to download it... + if ($uri_extension) { + $extension = $uri_extension; + } elseif (isset($path_parts['extension'])) { + $extension = $path_parts['extension']; + } else { + $extension = null; + } + + if ($extension) { + $download = true; + if (in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []), true)) { + $download = false; + } + Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download); + } + } + + // Nothing found + return false; + } +} diff --git a/system/src/Grav/Common/GravTrait.php b/system/src/Grav/Common/GravTrait.php new file mode 100644 index 0000000..4638905 --- /dev/null +++ b/system/src/Grav/Common/GravTrait.php @@ -0,0 +1,34 @@ + 'Grav CMS' + ]; + + public static function getClient(array $overrides = [], int $connections = 6, callable $callback = null): HttpClientInterface + { + $config = Grav::instance()['config']; + $options = static::getOptions(); + + // Use callback if provided + if ($callback) { + self::$callback = $callback; + $options->setOnProgress([Client::class, 'progress']); + } + + $settings = array_merge($options->toArray(), $overrides); + $preferred_method = $config->get('system.http.method'); + // Try old GPM setting if value is the same as system default + if ($preferred_method === 'auto') { + $preferred_method = $config->get('system.gpm.method', 'auto'); + } + + switch ($preferred_method) { + case 'curl': + $client = new CurlHttpClient($settings, $connections); + break; + case 'fopen': + case 'native': + $client = new NativeHttpClient($settings, $connections); + break; + default: + $client = HttpClient::create($settings, $connections); + } + + return $client; + } + + /** + * Get HTTP Options + * + * @return HttpOptions + */ + public static function getOptions(): HttpOptions + { + $config = Grav::instance()['config']; + $referer = defined('GRAV_CLI') ? 'grav_cli' : Grav::instance()['uri']->rootUrl(true); + + $options = new HttpOptions(); + + // Set default Headers + $options->setHeaders(array_merge([ 'Referer' => $referer ], self::$headers)); + + // Disable verify Peer if required + $verify_peer = $config->get('system.http.verify_peer'); + // Try old GPM setting if value is default + if ($verify_peer === true) { + $verify_peer = $config->get('system.gpm.verify_peer', null) ?? $verify_peer; + } + $options->verifyPeer($verify_peer); + + // Set verify Host + $verify_host = $config->get('system.http.verify_host', true); + $options->verifyHost($verify_host); + + // New setting and must be enabled for Proxy to work + if ($config->get('system.http.enable_proxy', true)) { + // Set proxy url if provided + $proxy_url = $config->get('system.http.proxy_url', $config->get('system.gpm.proxy_url', null)); + if ($proxy_url !== null) { + $options->setProxy($proxy_url); + } + + // Certificate + $proxy_cert = $config->get('system.http.proxy_cert_path', null); + if ($proxy_cert !== null) { + $options->setCaPath($proxy_cert); + } + } + + return $options; + } + + /** + * Progress normalized for cURL and Fopen + * Accepts a variable length of arguments passed in by stream method + * + * @return void + */ + public static function progress(int $bytes_transferred, int $filesize, array $info) + { + + if ($bytes_transferred > 0) { + $percent = $filesize <= 0 ? 0 : (int)(($bytes_transferred * 100) / $filesize); + + $progress = [ + 'code' => $info['http_code'], + 'filesize' => $filesize, + 'transferred' => $bytes_transferred, + 'percent' => $percent < 100 ? $percent : 100 + ]; + + if (self::$callback !== null) { + call_user_func(self::$callback, $progress); + } + } + } +} diff --git a/system/src/Grav/Common/HTTP/Response.php b/system/src/Grav/Common/HTTP/Response.php new file mode 100644 index 0000000..4a05136 --- /dev/null +++ b/system/src/Grav/Common/HTTP/Response.php @@ -0,0 +1,96 @@ +getContent(); + } + + + /** + * Makes a request to the URL by using the preferred method + * + * @param string $method method to call such as GET, PUT, etc + * @param string $uri URL to call + * @param array $overrides An array of parameters for both `curl` and `fopen` + * @param callable|null $callback Either a function or callback in array notation + * @return ResponseInterface + * @throws TransportExceptionInterface + */ + public static function request(string $method, string $uri, array $overrides = [], callable $callback = null): ResponseInterface + { + if (empty($method)) { + throw new TransportException('missing method (GET, PUT, etc.)'); + } + + if (empty($uri)) { + throw new TransportException('missing URI'); + } + + // check if this function is available, if so use it to stop any timeouts + try { + if (Utils::functionExists('set_time_limit')) { + @set_time_limit(0); + } + } catch (Exception $e) {} + + $client = Client::getClient($overrides, 6, $callback); + + return $client->request($method, $uri); + } + + + /** + * Is this a remote file or not + * + * @param string $file + * @return bool + */ + public static function isRemote($file): bool + { + return (bool) filter_var($file, FILTER_VALIDATE_URL); + } + + +} diff --git a/system/src/Grav/Common/Helpers/Base32.php b/system/src/Grav/Common/Helpers/Base32.php new file mode 100644 index 0000000..825c1ec --- /dev/null +++ b/system/src/Grav/Common/Helpers/Base32.php @@ -0,0 +1,141 @@ +', '?' + 0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G' + 0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O' + 0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W' + 0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_' + 0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g' + 0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o' + 0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w' + 0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL' + ]; + + /** + * Encode in Base32 + * + * @param string $bytes + * @return string + */ + public static function encode($bytes) + { + $i = 0; + $index = 0; + $base32 = ''; + $bytesLen = strlen($bytes); + + while ($i < $bytesLen) { + $currByte = ord($bytes[$i]); + + /* Is the current digit going to span a byte boundary? */ + if ($index > 3) { + if (($i + 1) < $bytesLen) { + $nextByte = ord($bytes[$i+1]); + } else { + $nextByte = 0; + } + + $digit = $currByte & (0xFF >> $index); + $index = ($index + 5) % 8; + $digit <<= $index; + $digit |= $nextByte >> (8 - $index); + $i++; + } else { + $digit = ($currByte >> (8 - ($index + 5))) & 0x1F; + $index = ($index + 5) % 8; + if ($index === 0) { + $i++; + } + } + + $base32 .= self::$base32Chars[$digit]; + } + return $base32; + } + + /** + * Decode in Base32 + * + * @param string $base32 + * @return string + */ + public static function decode($base32) + { + $bytes = []; + $base32Len = strlen($base32); + $base32LookupLen = count(self::$base32Lookup); + + for ($i = $base32Len * 5 / 8 - 1; $i >= 0; --$i) { + $bytes[] = 0; + } + + for ($i = 0, $index = 0, $offset = 0; $i < $base32Len; $i++) { + $lookup = ord($base32[$i]) - ord('0'); + + /* Skip chars outside the lookup table */ + if ($lookup < 0 || $lookup >= $base32LookupLen) { + continue; + } + + $digit = self::$base32Lookup[$lookup]; + + /* If this digit is not in the table, ignore it */ + if ($digit === 0xFF) { + continue; + } + + if ($index <= 3) { + $index = ($index + 5) % 8; + if ($index === 0) { + $bytes[$offset] |= $digit; + $offset++; + if ($offset >= count($bytes)) { + break; + } + } else { + $bytes[$offset] |= $digit << (8 - $index); + } + } else { + $index = ($index + 5) % 8; + $bytes[$offset] |= ($digit >> $index); + $offset++; + if ($offset >= count($bytes)) { + break; + } + $bytes[$offset] |= $digit << (8 - $index); + } + } + + $bites = ''; + foreach ($bytes as $byte) { + $bites .= chr($byte); + } + + return $bites; + } +} diff --git a/system/src/Grav/Common/Helpers/Excerpts.php b/system/src/Grav/Common/Helpers/Excerpts.php new file mode 100644 index 0000000..173850a --- /dev/null +++ b/system/src/Grav/Common/Helpers/Excerpts.php @@ -0,0 +1,196 @@ +` + * @param PageInterface|null $page Page, defaults to the current page object + * @return string Returns final HTML string + */ + public static function processImageHtml($html, PageInterface $page = null) + { + $excerpt = static::getExcerptFromHtml($html, 'img'); + if (null === $excerpt) { + return ''; + } + + $original_src = $excerpt['element']['attributes']['src']; + $excerpt['element']['attributes']['href'] = $original_src; + + $excerpt = static::processLinkExcerpt($excerpt, $page, 'image'); + + $excerpt['element']['attributes']['src'] = $excerpt['element']['attributes']['href']; + unset($excerpt['element']['attributes']['href']); + + $excerpt = static::processImageExcerpt($excerpt, $page); + + $excerpt['element']['attributes']['data-src'] = $original_src; + + $html = static::getHtmlFromExcerpt($excerpt); + + return $html; + } + + /** + * Process Grav page link URL from HTML tag + * + * @param string $html HTML tag e.g. `Page Link` + * @param PageInterface|null $page Page, defaults to the current page object + * @return string Returns final HTML string + */ + public static function processLinkHtml($html, PageInterface $page = null) + { + $excerpt = static::getExcerptFromHtml($html, 'a'); + if (null === $excerpt) { + return ''; + } + + $original_href = $excerpt['element']['attributes']['href']; + $excerpt = static::processLinkExcerpt($excerpt, $page, 'link'); + $excerpt['element']['attributes']['data-href'] = $original_href; + + $html = static::getHtmlFromExcerpt($excerpt); + + return $html; + } + + /** + * Get an Excerpt array from a chunk of HTML + * + * @param string $html Chunk of HTML + * @param string $tag A tag, for example `img` + * @return array|null returns nested array excerpt + */ + public static function getExcerptFromHtml($html, $tag) + { + $doc = new DOMDocument('1.0', 'UTF-8'); + $internalErrors = libxml_use_internal_errors(true); + $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')); + libxml_use_internal_errors($internalErrors); + + $elements = $doc->getElementsByTagName($tag); + $excerpt = null; + $inner = []; + + foreach ($elements as $element) { + $attributes = []; + foreach ($element->attributes as $name => $value) { + $attributes[$name] = $value->value; + } + $excerpt = [ + 'element' => [ + 'name' => $element->tagName, + 'attributes' => $attributes + ] + ]; + + foreach ($element->childNodes as $node) { + $inner[] = $doc->saveHTML($node); + } + + $excerpt = array_merge_recursive($excerpt, ['element' => ['text' => implode('', $inner)]]); + + + } + + return $excerpt; + } + + /** + * Rebuild HTML tag from an excerpt array + * + * @param array $excerpt + * @return string + */ + public static function getHtmlFromExcerpt($excerpt) + { + $element = $excerpt['element']; + $html = '<'.$element['name']; + + if (isset($element['attributes'])) { + foreach ($element['attributes'] as $name => $value) { + if ($value === null) { + continue; + } + $html .= ' '.$name.'="'.$value.'"'; + } + } + + if (isset($element['text'])) { + $html .= '>'; + $html .= is_array($element['text']) ? static::getHtmlFromExcerpt(['element' => $element['text']]) : $element['text']; + $html .= ''; + } else { + $html .= ' />'; + } + + return $html; + } + + /** + * Process a Link excerpt + * + * @param array $excerpt + * @param PageInterface|null $page Page, defaults to the current page object + * @param string $type + * @return mixed + */ + public static function processLinkExcerpt($excerpt, PageInterface $page = null, $type = 'link') + { + $excerpts = new ExcerptsObject($page); + + return $excerpts->processLinkExcerpt($excerpt, $type); + } + + /** + * Process an image excerpt + * + * @param array $excerpt + * @param PageInterface|null $page Page, defaults to the current page object + * @return array + */ + public static function processImageExcerpt(array $excerpt, PageInterface $page = null) + { + $excerpts = new ExcerptsObject($page); + + return $excerpts->processImageExcerpt($excerpt); + } + + /** + * Process media actions + * + * @param Medium $medium + * @param string|array $url + * @param PageInterface|null $page Page, defaults to the current page object + * @return Medium|Link + */ + public static function processMediaActions($medium, $url, PageInterface $page = null) + { + $excerpts = new ExcerptsObject($page); + + return $excerpts->processMediaActions($medium, $url); + } +} diff --git a/system/src/Grav/Common/Helpers/Exif.php b/system/src/Grav/Common/Helpers/Exif.php new file mode 100644 index 0000000..36e391f --- /dev/null +++ b/system/src/Grav/Common/Helpers/Exif.php @@ -0,0 +1,48 @@ +get('system.media.auto_metadata_exif')) { + if (function_exists('exif_read_data') && class_exists(Reader::class)) { + $this->reader = Reader::factory(Reader::TYPE_NATIVE); + } else { + throw new RuntimeException('Please enable the Exif extension for PHP or disable Exif support in Grav system configuration'); + } + } + } + + /** + * @return Reader + */ + public function getReader() + { + return $this->reader; + } +} diff --git a/system/src/Grav/Common/Helpers/LogViewer.php b/system/src/Grav/Common/Helpers/LogViewer.php new file mode 100644 index 0000000..187828a --- /dev/null +++ b/system/src/Grav/Common/Helpers/LogViewer.php @@ -0,0 +1,167 @@ +.*)\] (?P\w+).(?P\w+): (?P.*[^ ]+) (?P[^ ]+) (?P[^ ]+)/'; + + /** + * Get the objects of a tailed file + * + * @param string $filepath + * @param int $lines + * @param bool $desc + * @return array + */ + public function objectTail($filepath, $lines = 1, $desc = true) + { + $data = $this->tail($filepath, $lines); + $tailed_log = $data ? explode(PHP_EOL, $data) : []; + $line_objects = []; + + foreach ($tailed_log as $line) { + $line_objects[] = $this->parse($line); + } + + return $desc ? $line_objects : array_reverse($line_objects); + } + + /** + * Optimized way to get just the last few entries of a log file + * + * @param string $filepath + * @param int $lines + * @return string|false + */ + public function tail($filepath, $lines = 1) + { + $f = $filepath ? @fopen($filepath, 'rb') : false; + if ($f === false) { + return false; + } + + $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096)); + + fseek($f, -1, SEEK_END); + if (fread($f, 1) !== "\n") { + --$lines; + } + + // Start reading + $output = ''; + // While we would like more + while (ftell($f) > 0 && $lines >= 0) { + // Figure out how far back we should jump + $seek = min(ftell($f), $buffer); + // Do the jump (backwards, relative to where we are) + fseek($f, -$seek, SEEK_CUR); + // Read a chunk and prepend it to our output + $chunk = fread($f, $seek); + if ($chunk === false) { + throw new \RuntimeException('Cannot read file'); + } + $output = $chunk . $output; + // Jump back to where we started reading + fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR); + // Decrease our line counter + $lines -= substr_count($chunk, "\n"); + } + // While we have too many lines + // (Because of buffer size we might have read too many) + while ($lines++ < 0) { + // Find first newline and remove all text before that + $output = substr($output, strpos($output, "\n") + 1); + } + // Close file and return + fclose($f); + + return trim($output); + } + + /** + * Helper class to get level color + * + * @param string $level + * @return string + */ + public static function levelColor($level) + { + $colors = [ + 'DEBUG' => 'green', + 'INFO' => 'cyan', + 'NOTICE' => 'yellow', + 'WARNING' => 'yellow', + 'ERROR' => 'red', + 'CRITICAL' => 'red', + 'ALERT' => 'red', + 'EMERGENCY' => 'magenta' + ]; + return $colors[$level] ?? 'white'; + } + + /** + * Parse a monolog row into array bits + * + * @param string $line + * @return array + */ + public function parse($line) + { + if (!is_string($line) || $line === '') { + return []; + } + + preg_match($this->pattern, $line, $data); + if (!isset($data['date'])) { + return []; + } + + preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches); + if (is_array($matches) && isset($matches[1])) { + $data['message'] = trim($matches[1]); + $data['trace'] = trim($matches[2]); + } + + return [ + 'date' => DateTime::createFromFormat('Y-m-d H:i:s', $data['date']), + 'logger' => $data['logger'], + 'level' => $data['level'], + 'message' => $data['message'], + 'trace' => isset($data['trace']) ? $this->parseTrace($data['trace']) : null, + 'context' => json_decode($data['context'], true), + 'extra' => json_decode($data['extra'], true) + ]; + } + + /** + * Parse text of trace into an array of lines + * + * @param string $trace + * @param int $rows + * @return array + */ + public static function parseTrace($trace, $rows = 10) + { + $lines = array_filter(preg_split('/#\d*/m', $trace)); + + return array_slice($lines, 0, $rows); + } +} diff --git a/system/src/Grav/Common/Helpers/Truncator.php b/system/src/Grav/Common/Helpers/Truncator.php new file mode 100644 index 0000000..0a701b6 --- /dev/null +++ b/system/src/Grav/Common/Helpers/Truncator.php @@ -0,0 +1,344 @@ +getElementsByTagName('div')->item(0); + $container = $container->parentNode->removeChild($container); + + // Iterate over words. + $words = new DOMWordsIterator($container); + $truncated = false; + foreach ($words as $word) { + // If we have exceeded the limit, we delete the remainder of the content. + if ($words->key() >= $limit) { + // Grab current position. + $currentWordPosition = $words->currentWordPosition(); + $curNode = $currentWordPosition[0]; + $offset = $currentWordPosition[1]; + $words = $currentWordPosition[2]; + + $curNode->nodeValue = substr( + $curNode->nodeValue, + 0, + $words[$offset][1] + strlen($words[$offset][0]) + ); + + self::removeProceedingNodes($curNode, $container); + + if (!empty($ellipsis)) { + self::insertEllipsis($curNode, $ellipsis); + } + + $truncated = true; + + break; + } + } + + // Return original HTML if not truncated. + if ($truncated) { + $html = self::getCleanedHtml($doc, $container); + } + + return $html; + } + + /** + * Safely truncates HTML by a given number of letters. + * + * @param string $html Input HTML. + * @param int $limit Limit to how many letters we preserve. + * @param string $ellipsis String to use as ellipsis (if any). + * @return string Safe truncated HTML. + */ + public static function truncateLetters($html, $limit = 0, $ellipsis = '') + { + if ($limit <= 0) { + return $html; + } + + $doc = self::htmlToDomDocument($html); + $container = $doc->getElementsByTagName('div')->item(0); + $container = $container->parentNode->removeChild($container); + + // Iterate over letters. + $letters = new DOMLettersIterator($container); + $truncated = false; + foreach ($letters as $letter) { + // If we have exceeded the limit, we want to delete the remainder of this document. + if ($letters->key() >= $limit) { + $currentText = $letters->currentTextPosition(); + $currentText[0]->nodeValue = mb_substr($currentText[0]->nodeValue, 0, $currentText[1] + 1); + self::removeProceedingNodes($currentText[0], $container); + + if (!empty($ellipsis)) { + self::insertEllipsis($currentText[0], $ellipsis); + } + + $truncated = true; + + break; + } + } + + // Return original HTML if not truncated. + if ($truncated) { + $html = self::getCleanedHtml($doc, $container); + } + + return $html; + } + + /** + * Builds a DOMDocument object from a string containing HTML. + * + * @param string $html HTML to load + * @return DOMDocument Returns a DOMDocument object. + */ + public static function htmlToDomDocument($html) + { + if (!$html) { + $html = ''; + } + + // Transform multibyte entities which otherwise display incorrectly. + $html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); + + // Internal errors enabled as HTML5 not fully supported. + libxml_use_internal_errors(true); + + // Instantiate new DOMDocument object, and then load in UTF-8 HTML. + $dom = new DOMDocument(); + $dom->encoding = 'UTF-8'; + $dom->loadHTML("
$html
"); + + return $dom; + } + + /** + * Removes all nodes after the current node. + * + * @param DOMNode|DOMElement $domNode + * @param DOMNode|DOMElement $topNode + * @return void + */ + private static function removeProceedingNodes($domNode, $topNode) + { + /** @var DOMNode|null $nextNode */ + $nextNode = $domNode->nextSibling; + + if ($nextNode !== null) { + self::removeProceedingNodes($nextNode, $topNode); + $domNode->parentNode->removeChild($nextNode); + } else { + //scan upwards till we find a sibling + $curNode = $domNode->parentNode; + while ($curNode !== $topNode) { + if ($curNode->nextSibling !== null) { + $curNode = $curNode->nextSibling; + self::removeProceedingNodes($curNode, $topNode); + $curNode->parentNode->removeChild($curNode); + break; + } + $curNode = $curNode->parentNode; + } + } + } + + /** + * Clean extra code + * + * @param DOMDocument $doc + * @param DOMNode $container + * @return string + */ + private static function getCleanedHTML(DOMDocument $doc, DOMNode $container) + { + while ($doc->firstChild) { + $doc->removeChild($doc->firstChild); + } + + while ($container->firstChild) { + $doc->appendChild($container->firstChild); + } + + return trim($doc->saveHTML()); + } + + /** + * Inserts an ellipsis + * + * @param DOMNode|DOMElement $domNode Element to insert after. + * @param string $ellipsis Text used to suffix our document. + * @return void + */ + private static function insertEllipsis($domNode, $ellipsis) + { + $avoid = array('a', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5'); //html tags to avoid appending the ellipsis to + + if ($domNode->parentNode->parentNode !== null && in_array($domNode->parentNode->nodeName, $avoid, true)) { + // Append as text node to parent instead + $textNode = new DOMText($ellipsis); + + /** @var DOMNode|null $nextSibling */ + $nextSibling = $domNode->parentNode->parentNode->nextSibling; + if ($nextSibling) { + $domNode->parentNode->parentNode->insertBefore($textNode, $domNode->parentNode->parentNode->nextSibling); + } else { + $domNode->parentNode->parentNode->appendChild($textNode); + } + } else { + // Append to current node + $domNode->nodeValue = rtrim($domNode->nodeValue) . $ellipsis; + } + } + + /** + * @param string $text + * @param int $length + * @param string $ending + * @param bool $exact + * @param bool $considerHtml + * @return string + */ + public function truncate( + $text, + $length = 100, + $ending = '...', + $exact = false, + $considerHtml = true + ) { + if ($considerHtml) { + // if the plain text is shorter than the maximum length, return the whole text + if (strlen(preg_replace('/<.*?>/', '', $text)) <= $length) { + return $text; + } + + // splits all html-tags to scanable lines + preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER); + $total_length = strlen($ending); + $truncate = ''; + $open_tags = []; + + foreach ($lines as $line_matchings) { + // if there is any html-tag in this line, handle it and add it (uncounted) to the output + if (!empty($line_matchings[1])) { + // if it's an "empty element" with or without xhtml-conform closing slash + if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) { + // do nothing + // if tag is a closing tag + } elseif (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) { + // delete tag from $open_tags list + $pos = array_search($tag_matchings[1], $open_tags); + if ($pos !== false) { + unset($open_tags[$pos]); + } + // if tag is an opening tag + } elseif (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) { + // add tag to the beginning of $open_tags list + array_unshift($open_tags, strtolower($tag_matchings[1])); + } + // add html-tag to $truncate'd text + $truncate .= $line_matchings[1]; + } + // calculate the length of the plain text part of the line; handle entities as one character + $content_length = strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|[0-9a-f]{1,6};/i', ' ', $line_matchings[2])); + if ($total_length+$content_length> $length) { + // the number of characters which are left + $left = $length - $total_length; + $entities_length = 0; + // search for html entities + if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|[0-9a-f]{1,6};/i', $line_matchings[2], $entities, PREG_OFFSET_CAPTURE)) { + // calculate the real length of all entities in the legal range + foreach ($entities[0] as $entity) { + if ($entity[1]+1-$entities_length <= $left) { + $left--; + $entities_length += strlen($entity[0]); + } else { + // no more characters left + break; + } + } + } + $truncate .= substr($line_matchings[2], 0, $left+$entities_length); + // maximum lenght is reached, so get off the loop + break; + } else { + $truncate .= $line_matchings[2]; + $total_length += $content_length; + } + // if the maximum length is reached, get off the loop + if ($total_length>= $length) { + break; + } + } + } else { + if (strlen($text) <= $length) { + return $text; + } + + $truncate = substr($text, 0, $length - strlen($ending)); + } + // if the words shouldn't be cut in the middle... + if (!$exact) { + // ...search the last occurance of a space... + $spacepos = strrpos($truncate, ' '); + if (false !== $spacepos) { + // ...and cut the text in this position + $truncate = substr($truncate, 0, $spacepos); + } + } + // add the defined ending to the text + $truncate .= $ending; + if (isset($open_tags)) { + // close all unclosed html-tags + foreach ($open_tags as $tag) { + $truncate .= ''; + } + } + + return $truncate; + } +} diff --git a/system/src/Grav/Common/Helpers/YamlLinter.php b/system/src/Grav/Common/Helpers/YamlLinter.php new file mode 100644 index 0000000..45b5144 --- /dev/null +++ b/system/src/Grav/Common/Helpers/YamlLinter.php @@ -0,0 +1,121 @@ +get('system.pages.theme'); + $theme_path = 'themes://' . $current_theme . '/blueprints'; + + $locator->addPath('blueprints', '', [$theme_path]); + return static::recurseFolder('blueprints://'); + } + + /** + * @param string $path + * @param string $extensions + * @return array + */ + public static function recurseFolder($path, $extensions = '(md|yaml)') + { + $lint_errors = []; + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $flags = RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::FOLLOW_SYMLINKS; + if ($locator->isStream($path)) { + $directory = $locator->getRecursiveIterator($path, $flags); + } else { + $directory = new RecursiveDirectoryIterator($path, $flags); + } + $recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); + $iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/ui'); + + /** @var RecursiveDirectoryIterator $file */ + foreach ($iterator as $filepath => $file) { + try { + Yaml::parse(static::extractYaml($filepath)); + } catch (Exception $e) { + $lint_errors[str_replace(GRAV_ROOT, '', $filepath)] = $e->getMessage(); + } + } + + return $lint_errors; + } + + /** + * @param string $path + * @return string + */ + protected static function extractYaml($path) + { + $extension = pathinfo($path, PATHINFO_EXTENSION); + if ($extension === 'md') { + $file = MarkdownFile::instance($path); + $contents = $file->frontmatter(); + $file->free(); + } else { + $contents = file_get_contents($path); + } + return $contents; + } +} diff --git a/system/src/Grav/Common/Inflector.php b/system/src/Grav/Common/Inflector.php new file mode 100644 index 0000000..50c218f --- /dev/null +++ b/system/src/Grav/Common/Inflector.php @@ -0,0 +1,357 @@ +isDebug()) { + static::$plural = $language->translate('GRAV.INFLECTOR_PLURALS', null, true); + static::$singular = $language->translate('GRAV.INFLECTOR_SINGULAR', null, true); + static::$uncountable = $language->translate('GRAV.INFLECTOR_UNCOUNTABLE', null, true); + static::$irregular = $language->translate('GRAV.INFLECTOR_IRREGULAR', null, true); + static::$ordinals = $language->translate('GRAV.INFLECTOR_ORDINALS', null, true); + } + } + } + + /** + * Pluralizes English nouns. + * + * @param string $word English noun to pluralize + * @param int $count The count + * @return string|false Plural noun + */ + public static function pluralize($word, $count = 2) + { + static::init(); + + if ((int)$count === 1) { + return $word; + } + + $lowercased_word = strtolower($word); + + if (is_array(static::$uncountable)) { + foreach (static::$uncountable as $_uncountable) { + if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) { + return $word; + } + } + } + + if (is_array(static::$irregular)) { + foreach (static::$irregular as $_plural => $_singular) { + if (preg_match('/(' . $_plural . ')$/i', $word, $arr)) { + return preg_replace('/(' . $_plural . ')$/i', substr($arr[0], 0, 1) . substr($_singular, 1), $word); + } + } + } + + if (is_array(static::$plural)) { + foreach (static::$plural as $rule => $replacement) { + if (preg_match($rule, $word)) { + return preg_replace($rule, $replacement, $word); + } + } + } + + return false; + } + + /** + * Singularizes English nouns. + * + * @param string $word English noun to singularize + * @param int $count + * + * @return string Singular noun. + */ + public static function singularize($word, $count = 1) + { + static::init(); + + if ((int)$count !== 1) { + return $word; + } + + $lowercased_word = strtolower($word); + + if (is_array(static::$uncountable)) { + foreach (static::$uncountable as $_uncountable) { + if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) { + return $word; + } + } + } + + if (is_array(static::$irregular)) { + foreach (static::$irregular as $_plural => $_singular) { + if (preg_match('/(' . $_singular . ')$/i', $word, $arr)) { + return preg_replace('/(' . $_singular . ')$/i', substr($arr[0], 0, 1) . substr($_plural, 1), $word); + } + } + } + + if (is_array(static::$singular)) { + foreach (static::$singular as $rule => $replacement) { + if (preg_match($rule, $word)) { + return preg_replace($rule, $replacement, $word); + } + } + } + + return $word; + } + + /** + * Converts an underscored or CamelCase word into a English + * sentence. + * + * The titleize public function converts text like "WelcomePage", + * "welcome_page" or "welcome page" to this "Welcome + * Page". + * If second parameter is set to 'first' it will only + * capitalize the first character of the title. + * + * @param string $word Word to format as tile + * @param string $uppercase If set to 'first' it will only uppercase the + * first character. Otherwise it will uppercase all + * the words in the title. + * + * @return string Text formatted as title + */ + public static function titleize($word, $uppercase = '') + { + $uppercase = $uppercase === 'first' ? 'ucfirst' : 'ucwords'; + + return $uppercase(static::humanize(static::underscorize($word))); + } + + /** + * Returns given word as CamelCased + * + * Converts a word like "send_email" to "SendEmail". It + * will remove non alphanumeric character from the word, so + * "who's online" will be converted to "WhoSOnline" + * + * @see variablize + * + * @param string $word Word to convert to camel case + * @return string UpperCamelCasedWord + */ + public static function camelize($word) + { + return str_replace(' ', '', ucwords(preg_replace('/[^A-Z^a-z^0-9]+/', ' ', $word))); + } + + /** + * Converts a word "into_it_s_underscored_version" + * + * Convert any "CamelCased" or "ordinary Word" into an + * "underscored_word". + * + * This can be really useful for creating friendly URLs. + * + * @param string $word Word to underscore + * @return string Underscored word + */ + public static function underscorize($word) + { + $regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1_\2', $word); + $regex2 = preg_replace('/([a-zd])([A-Z])/', '\1_\2', $regex1); + $regex3 = preg_replace('/[^A-Z^a-z^0-9]+/', '_', $regex2); + + return strtolower($regex3); + } + + /** + * Converts a word "into-it-s-hyphenated-version" + * + * Convert any "CamelCased" or "ordinary Word" into an + * "hyphenated-word". + * + * This can be really useful for creating friendly URLs. + * + * @param string $word Word to hyphenate + * @return string hyphenized word + */ + public static function hyphenize($word) + { + $regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1-\2', $word); + $regex2 = preg_replace('/([a-z])([A-Z])/', '\1-\2', $regex1); + $regex3 = preg_replace('/([0-9])([A-Z])/', '\1-\2', $regex2); + $regex4 = preg_replace('/[^A-Z^a-z^0-9]+/', '-', $regex3); + + $regex4 = trim($regex4, '-'); + + return strtolower($regex4); + } + + /** + * Returns a human-readable string from $word + * + * Returns a human-readable string from $word, by replacing + * underscores with a space, and by upper-casing the initial + * character by default. + * + * If you need to uppercase all the words you just have to + * pass 'all' as a second parameter. + * + * @param string $word String to "humanize" + * @param string $uppercase If set to 'all' it will uppercase all the words + * instead of just the first one. + * + * @return string Human-readable word + */ + public static function humanize($word, $uppercase = '') + { + $uppercase = $uppercase === 'all' ? 'ucwords' : 'ucfirst'; + + return $uppercase(str_replace('_', ' ', preg_replace('/_id$/', '', $word))); + } + + /** + * Same as camelize but first char is underscored + * + * Converts a word like "send_email" to "sendEmail". It + * will remove non alphanumeric character from the word, so + * "who's online" will be converted to "whoSOnline" + * + * @see camelize + * + * @param string $word Word to lowerCamelCase + * @return string Returns a lowerCamelCasedWord + */ + public static function variablize($word) + { + $word = static::camelize($word); + + return strtolower($word[0]) . substr($word, 1); + } + + /** + * Converts a class name to its table name according to rails + * naming conventions. + * + * Converts "Person" to "people" + * + * @see classify + * + * @param string $class_name Class name for getting related table_name. + * @return string plural_table_name + */ + public static function tableize($class_name) + { + return static::pluralize(static::underscorize($class_name)); + } + + /** + * Converts a table name to its class name according to rails + * naming conventions. + * + * Converts "people" to "Person" + * + * @see tableize + * + * @param string $table_name Table name for getting related ClassName. + * @return string SingularClassName + */ + public static function classify($table_name) + { + return static::camelize(static::singularize($table_name)); + } + + /** + * Converts number to its ordinal English form. + * + * This method converts 13 to 13th, 2 to 2nd ... + * + * @param int $number Number to get its ordinal value + * @return string Ordinal representation of given string. + */ + public static function ordinalize($number) + { + if (!is_array(static::$ordinals)) { + return (string)$number; + } + + static::init(); + + if (in_array($number % 100, range(11, 13), true)) { + return $number . static::$ordinals['default']; + } + + switch ($number % 10) { + case 1: + return $number . static::$ordinals['first']; + case 2: + return $number . static::$ordinals['second']; + case 3: + return $number . static::$ordinals['third']; + default: + return $number . static::$ordinals['default']; + } + } + + /** + * Converts a number of days to a number of months + * + * @param int $days + * @return int + */ + public static function monthize($days) + { + $now = new DateTime(); + $end = new DateTime(); + + $duration = new DateInterval("P{$days}D"); + + $diff = $end->add($duration)->diff($now); + + // handle years + if ($diff->y > 0) { + $diff->m += 12 * $diff->y; + } + + return $diff->m; + } +} diff --git a/system/src/Grav/Common/Iterator.php b/system/src/Grav/Common/Iterator.php new file mode 100644 index 0000000..6e80b94 --- /dev/null +++ b/system/src/Grav/Common/Iterator.php @@ -0,0 +1,261 @@ +items[$key] ?? null; + } + + /** + * Clone the iterator. + */ + public function __clone() + { + foreach ($this as $key => $value) { + if (is_object($value)) { + $this->{$key} = clone $this->{$key}; + } + } + } + + /** + * Convents iterator to a comma separated list. + * + * @return string + */ + public function __toString() + { + return implode(',', $this->items); + } + + /** + * Remove item from the list. + * + * @param string $key + * @return void + */ + public function remove($key) + { + $this->offsetUnset($key); + } + + /** + * Return previous item. + * + * @return mixed + */ + public function prev() + { + return prev($this->items); + } + + /** + * Return nth item. + * + * @param int $key + * @return mixed|bool + */ + public function nth($key) + { + $items = array_keys($this->items); + + return isset($items[$key]) ? $this->offsetGet($items[$key]) : false; + } + + /** + * Get the first item + * + * @return mixed + */ + public function first() + { + $items = array_keys($this->items); + + return $this->offsetGet(array_shift($items)); + } + + /** + * Get the last item + * + * @return mixed + */ + public function last() + { + $items = array_keys($this->items); + + return $this->offsetGet(array_pop($items)); + } + + /** + * Reverse the Iterator + * + * @return $this + */ + public function reverse() + { + $this->items = array_reverse($this->items); + + return $this; + } + + /** + * @param mixed $needle Searched value. + * + * @return string|int|false Key if found, otherwise false. + */ + public function indexOf($needle) + { + foreach (array_values($this->items) as $key => $value) { + if ($value === $needle) { + return $key; + } + } + + return false; + } + + /** + * Shuffle items. + * + * @return $this + */ + public function shuffle() + { + $keys = array_keys($this->items); + shuffle($keys); + + $new = []; + foreach ($keys as $key) { + $new[$key] = $this->items[$key]; + } + + $this->items = $new; + + return $this; + } + + /** + * Slice the list. + * + * @param int $offset + * @param int|null $length + * @return $this + */ + public function slice($offset, $length = null) + { + $this->items = array_slice($this->items, $offset, $length); + + return $this; + } + + /** + * Pick one or more random entries. + * + * @param int $num Specifies how many entries should be picked. + * @return $this + */ + public function random($num = 1) + { + $count = count($this->items); + if ($num > $count) { + $num = $count; + } + + $this->items = array_intersect_key($this->items, array_flip((array)array_rand($this->items, $num))); + + return $this; + } + + /** + * Append new elements to the list. + * + * @param array|Iterator $items Items to be appended. Existing keys will be overridden with the new values. + * @return $this + */ + public function append($items) + { + if ($items instanceof static) { + $items = $items->toArray(); + } + $this->items = array_merge($this->items, (array)$items); + + return $this; + } + + /** + * Filter elements from the list + * + * @param callable|null $callback A function the receives ($value, $key) and must return a boolean to indicate + * filter status + * + * @return $this + */ + public function filter(callable $callback = null) + { + foreach ($this->items as $key => $value) { + if ((!$callback && !(bool)$value) || ($callback && !$callback($value, $key))) { + unset($this->items[$key]); + } + } + + return $this; + } + + + /** + * Sorts elements from the list and returns a copy of the list in the proper order + * + * @param callable|null $callback + * @param bool $desc + * @return $this|array + * + */ + public function sort(callable $callback = null, $desc = false) + { + if (!$callback || !is_callable($callback)) { + return $this; + } + + $items = $this->items; + uasort($items, $callback); + + return !$desc ? $items : array_reverse($items, true); + } +} diff --git a/system/src/Grav/Common/Language/Language.php b/system/src/Grav/Common/Language/Language.php new file mode 100644 index 0000000..6709754 --- /dev/null +++ b/system/src/Grav/Common/Language/Language.php @@ -0,0 +1,662 @@ +grav = $grav; + $this->config = $grav['config']; + + $languages = $this->config->get('system.languages.supported', []); + foreach ($languages as &$language) { + $language = (string)$language; + } + unset($language); + + $this->languages = $languages; + + $this->init(); + } + + /** + * Initialize the default and enabled languages + * + * @return void + */ + public function init() + { + $default = $this->config->get('system.languages.default_lang'); + if (null !== $default) { + $default = (string)$default; + } + + // Note that reset returns false on empty languages. + $this->default = $default ?? reset($this->languages); + + $this->resetFallbackPageExtensions(); + + if (empty($this->languages)) { + // If no languages are set, turn of multi-language support. + $this->enabled = false; + } elseif ($default && !in_array($default, $this->languages, true)) { + // If default language isn't in the language list, we need to add it. + array_unshift($this->languages, $default); + } + } + + /** + * Ensure that languages are enabled + * + * @return bool + */ + public function enabled() + { + return $this->enabled; + } + + /** + * Returns true if language debugging is turned on. + * + * @return bool + */ + public function isDebug(): bool + { + return !$this->config->get('system.languages.translations', true); + } + + /** + * Gets the array of supported languages + * + * @return array + */ + public function getLanguages() + { + return $this->languages; + } + + /** + * Sets the current supported languages manually + * + * @param array $langs + * @return void + */ + public function setLanguages($langs) + { + $this->languages = $langs; + + $this->init(); + } + + /** + * Gets a pipe-separated string of available languages + * + * @param string|null $delimiter Delimiter to be quoted. + * @return string + */ + public function getAvailable($delimiter = null) + { + $languagesArray = $this->languages; //Make local copy + + $languagesArray = array_map(static function ($value) use ($delimiter) { + return preg_quote($value, $delimiter); + }, $languagesArray); + + sort($languagesArray); + + return implode('|', array_reverse($languagesArray)); + } + + /** + * Gets language, active if set, else default + * + * @return string|false + */ + public function getLanguage() + { + return $this->active ?: $this->default; + } + + /** + * Gets current default language + * + * @return string|false + */ + public function getDefault() + { + return $this->default; + } + + /** + * Sets default language manually + * + * @param string $lang + * @return string|bool + */ + public function setDefault($lang) + { + $lang = (string)$lang; + if ($this->validate($lang)) { + $this->default = $lang; + + return $lang; + } + + return false; + } + + /** + * Gets current active language + * + * @return string|false + */ + public function getActive() + { + return $this->active; + } + + /** + * Sets active language manually + * + * @param string|false $lang + * @return string|false + */ + public function setActive($lang) + { + $lang = (string)$lang; + if ($this->validate($lang)) { + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addMessage('Active language set to ' . $lang, 'debug'); + + $this->active = $lang; + + return $lang; + } + + return false; + } + + /** + * Sets the active language based on the first part of the URL + * + * @param string $uri + * @return string + */ + public function setActiveFromUri($uri) + { + $regex = '/(^\/(' . $this->getAvailable() . '))(?:\/|\?|$)/i'; + + // if languages set + if ($this->enabled()) { + // Try setting language from prefix of URL (/en/blah/blah). + if (preg_match($regex, $uri, $matches)) { + $this->lang_in_url = true; + $this->setActive($matches[2]); + $uri = preg_replace("/\\" . $matches[1] . '/', '', $uri, 1); + + // Store in session if language is different. + if (isset($this->grav['session']) && $this->grav['session']->isStarted() + && $this->config->get('system.languages.session_store_active', true) + && $this->grav['session']->active_language != $this->active + ) { + $this->grav['session']->active_language = $this->active; + } + } else { + // Try getting language from the session, else no active. + if (isset($this->grav['session']) && $this->grav['session']->isStarted() && + $this->config->get('system.languages.session_store_active', true)) { + $this->setActive($this->grav['session']->active_language ?: null); + } + // if still null, try from http_accept_language header + if ($this->active === null && + $this->config->get('system.languages.http_accept_language') && + $accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? false) { + $negotiator = new LanguageNegotiator(); + $best_language = $negotiator->getBest($accept, $this->languages); + + if ($best_language instanceof AcceptLanguage) { + $this->setActive($best_language->getType()); + } else { + $this->setActive($this->getDefault()); + } + } + } + } + + return $uri; + } + + /** + * Get a URL prefix based on configuration + * + * @param string|null $lang + * @return string + */ + public function getLanguageURLPrefix($lang = null) + { + if (!$this->enabled()) { + return ''; + } + + // if active lang is not passed in, use current active + if (!$lang) { + $lang = $this->getLanguage(); + } + + return $this->isIncludeDefaultLanguage($lang) ? '/' . $lang : ''; + } + + /** + * Test to see if language is default and language should be included in the URL + * + * @param string|null $lang + * @return bool + */ + public function isIncludeDefaultLanguage($lang = null) + { + if (!$this->enabled()) { + return false; + } + + // if active lang is not passed in, use current active + if (!$lang) { + $lang = $this->getLanguage(); + } + + return !($this->default === $lang && $this->config->get('system.languages.include_default_lang') === false); + } + + /** + * Simple getter to tell if a language was found in the URL + * + * @return bool + */ + public function isLanguageInUrl() + { + return (bool) $this->lang_in_url; + } + + /** + * Get full list of used language page extensions: [''=>'.md', 'en'=>'.en.md', ...] + * + * @param string|null $fileExtension + * @return array + */ + public function getPageExtensions($fileExtension = null) + { + $fileExtension = $fileExtension ?: CONTENT_EXT; + + if (!isset($this->fallback_extensions[$fileExtension])) { + $extensions[''] = $fileExtension; + foreach ($this->languages as $code) { + $extensions[$code] = ".{$code}{$fileExtension}"; + } + + $this->fallback_extensions[$fileExtension] = $extensions; + } + + return $this->fallback_extensions[$fileExtension]; + } + + /** + * Gets an array of valid extensions with active first, then fallback extensions + * + * @param string|null $fileExtension + * @param string|null $languageCode + * @param bool $assoc Return values in ['en' => '.en.md', ...] format. + * @return array Key is the language code, value is the file extension to be used. + */ + public function getFallbackPageExtensions(string $fileExtension = null, string $languageCode = null, bool $assoc = false) + { + $fileExtension = $fileExtension ?: CONTENT_EXT; + $key = $fileExtension . '-' . ($languageCode ?? 'default') . '-' . (int)$assoc; + + if (!isset($this->fallback_extensions[$key])) { + $all = $this->getPageExtensions($fileExtension); + $list = []; + $fallback = $this->getFallbackLanguages($languageCode, true); + foreach ($fallback as $code) { + $ext = $all[$code] ?? null; + if (null !== $ext) { + $list[$code] = $ext; + } + } + if (!$assoc) { + $list = array_values($list); + } + + $this->fallback_extensions[$key] = $list; + } + + return $this->fallback_extensions[$key]; + } + + /** + * Resets the fallback_languages value. + * + * Useful to re-initialize the pages and change site language at runtime, example: + * + * ``` + * $this->grav['language']->setActive('it'); + * $this->grav['language']->resetFallbackPageExtensions(); + * $this->grav['pages']->init(); + * ``` + * + * @return void + */ + public function resetFallbackPageExtensions() + { + $this->fallback_languages = []; + $this->fallback_extensions = []; + $this->page_extensions = []; + } + + /** + * Gets an array of languages with active first, then fallback languages. + * + * + * @param string|null $languageCode + * @param bool $includeDefault If true, list contains '', which can be used for default + * @return array + */ + public function getFallbackLanguages(string $languageCode = null, bool $includeDefault = false) + { + // Handle default. + if ($languageCode === '' || !$this->enabled()) { + return ['']; + } + + $default = $this->getDefault() ?? 'en'; + $active = $languageCode ?? $this->getActive() ?? $default; + $key = $active . '-' . (int)$includeDefault; + + if (!isset($this->fallback_languages[$key])) { + $fallback = $this->config->get('system.languages.content_fallback.' . $active); + $fallback_languages = []; + + if (null === $fallback && $this->config->get('system.languages.pages_fallback_only', false)) { + user_error('Configuration option `system.languages.pages_fallback_only` is deprecated since Grav 1.7, use `system.languages.content_fallback` instead', E_USER_DEPRECATED); + + // Special fallback list returns itself and all the previous items in reverse order: + // active: 'v2', languages: ['v1', 'v2', 'v3', 'v4'] => ['v2', 'v1', ''] + if ($includeDefault) { + $fallback_languages[''] = ''; + } + foreach ($this->languages as $code) { + $fallback_languages[$code] = $code; + if ($code === $active) { + break; + } + } + $fallback_languages = array_reverse($fallback_languages); + } else { + if (null === $fallback) { + $fallback = [$default]; + } elseif (!is_array($fallback)) { + $fallback = is_string($fallback) && $fallback !== '' ? explode(',', $fallback) : []; + } + array_unshift($fallback, $active); + $fallback = array_unique($fallback); + + foreach ($fallback as $code) { + // Default fallback list has active language followed by default language and extensionless file: + // active: 'fi', default: 'en', languages: ['sv', 'en', 'de', 'fi'] => ['fi', 'en', ''] + $fallback_languages[$code] = $code; + if ($includeDefault && $code === $default) { + $fallback_languages[''] = ''; + } + } + } + + $fallback_languages = array_values($fallback_languages); + + $this->fallback_languages[$key] = $fallback_languages; + } + + return $this->fallback_languages[$key]; + } + + /** + * Ensures the language is valid and supported + * + * @param string $lang + * @return bool + */ + public function validate($lang) + { + return in_array($lang, $this->languages, true); + } + + /** + * Translate a key and possibly arguments into a string using current lang and fallbacks + * + * @param string|array $args The first argument is the lookup key value + * Other arguments can be passed and replaced in the translation with sprintf syntax + * @param array|null $languages + * @param bool $array_support + * @param bool $html_out + * @return string|string[] + */ + public function translate($args, array $languages = null, $array_support = false, $html_out = false) + { + if (is_array($args)) { + $lookup = array_shift($args); + } else { + $lookup = $args; + $args = []; + } + + if (!$this->isDebug()) { + if ($lookup && $this->enabled() && empty($languages)) { + $languages = $this->getTranslatedLanguages(); + } + + $languages = $languages ?: ['en']; + + foreach ((array)$languages as $lang) { + $translation = $this->getTranslation($lang, $lookup, $array_support); + + if ($translation) { + if (is_string($translation) && count($args) >= 1) { + return vsprintf($translation, $args); + } + + return $translation; + } + } + } elseif ($array_support) { + return [$lookup]; + } + + if ($html_out) { + return '' . $lookup . ''; + } + + return $lookup; + } + + /** + * Translate Array + * + * @param string $key + * @param string $index + * @param array|null $languages + * @param bool $html_out + * @return string + */ + public function translateArray($key, $index, $languages = null, $html_out = false) + { + if ($this->isDebug()) { + return $key . '[' . $index . ']'; + } + + if ($key && empty($languages) && $this->enabled()) { + $languages = $this->getTranslatedLanguages(); + } + + $languages = $languages ?: ['en']; + + foreach ((array)$languages as $lang) { + $translation_array = (array)Grav::instance()['languages']->get($lang . '.' . $key, null); + if ($translation_array && array_key_exists($index, $translation_array)) { + return $translation_array[$index]; + } + } + + if ($html_out) { + return '' . $key . '[' . $index . ']'; + } + + return $key . '[' . $index . ']'; + } + + /** + * Lookup the translation text for a given lang and key + * + * @param string $lang lang code + * @param string $key key to lookup with + * @param bool $array_support + * @return string|string[] + */ + public function getTranslation($lang, $key, $array_support = false) + { + if ($this->isDebug()) { + return $key; + } + + $translation = Grav::instance()['languages']->get($lang . '.' . $key, null); + if (!$array_support && is_array($translation)) { + return (string)array_shift($translation); + } + + return $translation; + } + + /** + * Get the browser accepted languages + * + * @param array $accept_langs + * @return array + * @deprecated 1.6 No longer used - using content negotiation. + */ + public function getBrowserLanguages($accept_langs = []) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, no longer used', E_USER_DEPRECATED); + + if (empty($this->http_accept_language)) { + if (empty($accept_langs) && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + $accept_langs = $_SERVER['HTTP_ACCEPT_LANGUAGE']; + } else { + return $accept_langs; + } + + $langs = []; + + foreach (explode(',', $accept_langs) as $k => $pref) { + // split $pref again by ';q=' + // and decorate the language entries by inverted position + if (false !== ($i = strpos($pref, ';q='))) { + $langs[substr($pref, 0, $i)] = [(float)substr($pref, $i + 3), -$k]; + } else { + $langs[$pref] = [1, -$k]; + } + } + arsort($langs); + + // no need to undecorate, because we're only interested in the keys + $this->http_accept_language = array_keys($langs); + } + return $this->http_accept_language; + } + + /** + * Accessible wrapper to LanguageCodes + * + * @param string $code + * @param string $type + * @return string|false + */ + public function getLanguageCode($code, $type = 'name') + { + return LanguageCodes::get($code, $type); + } + + /** + * @return array + */ + public function __debugInfo() + { + $vars = get_object_vars($this); + unset($vars['grav'], $vars['config']); + + return $vars; + } + + /** + * @return array + */ + protected function getTranslatedLanguages(): array + { + if ($this->config->get('system.languages.translations_fallback', true)) { + $languages = $this->getFallbackLanguages(); + } else { + $languages = [$this->getLanguage()]; + } + + $languages[] = 'en'; + + return array_values(array_unique($languages)); + } +} diff --git a/system/src/Grav/Common/Language/LanguageCodes.php b/system/src/Grav/Common/Language/LanguageCodes.php new file mode 100644 index 0000000..e637bdd --- /dev/null +++ b/system/src/Grav/Common/Language/LanguageCodes.php @@ -0,0 +1,244 @@ + [ 'name' => 'Afrikaans', 'nativeName' => 'Afrikaans' ], + 'ak' => [ 'name' => 'Akan', 'nativeName' => 'Akan' ], // unverified native name + 'ast' => [ 'name' => 'Asturian', 'nativeName' => 'Asturianu' ], + 'ar' => [ 'name' => 'Arabic', 'nativeName' => 'عربي', 'orientation' => 'rtl'], + 'as' => [ 'name' => 'Assamese', 'nativeName' => 'অসমীয়া' ], + 'be' => [ 'name' => 'Belarusian', 'nativeName' => 'Беларуская' ], + 'bg' => [ 'name' => 'Bulgarian', 'nativeName' => 'Български' ], + 'bn' => [ 'name' => 'Bengali', 'nativeName' => 'বাংলা' ], + 'bn-BD' => [ 'name' => 'Bengali (Bangladesh)', 'nativeName' => 'বাংলা (বাংলাদেশ)' ], + 'bn-IN' => [ 'name' => 'Bengali (India)', 'nativeName' => 'বাংলা (ভারত)' ], + 'br' => [ 'name' => 'Breton', 'nativeName' => 'Brezhoneg' ], + 'bs' => [ 'name' => 'Bosnian', 'nativeName' => 'Bosanski' ], + 'ca' => [ 'name' => 'Catalan', 'nativeName' => 'Català' ], + 'ca-valencia'=> [ 'name' => 'Catalan (Valencian)', 'nativeName' => 'Català (valencià)' ], // not iso-639-1. a=l10n-drivers + 'cs' => [ 'name' => 'Czech', 'nativeName' => 'Čeština' ], + 'cy' => [ 'name' => 'Welsh', 'nativeName' => 'Cymraeg' ], + 'da' => [ 'name' => 'Danish', 'nativeName' => 'Dansk' ], + 'de' => [ 'name' => 'German', 'nativeName' => 'Deutsch' ], + 'de-AT' => [ 'name' => 'German (Austria)', 'nativeName' => 'Deutsch (Österreich)' ], + 'de-CH' => [ 'name' => 'German (Switzerland)', 'nativeName' => 'Deutsch (Schweiz)' ], + 'de-DE' => [ 'name' => 'German (Germany)', 'nativeName' => 'Deutsch (Deutschland)' ], + 'dsb' => [ 'name' => 'Lower Sorbian', 'nativeName' => 'Dolnoserbšćina' ], // iso-639-2 + 'el' => [ 'name' => 'Greek', 'nativeName' => 'Ελληνικά' ], + 'en' => [ 'name' => 'English', 'nativeName' => 'English' ], + 'en-AU' => [ 'name' => 'English (Australian)', 'nativeName' => 'English (Australian)' ], + 'en-CA' => [ 'name' => 'English (Canadian)', 'nativeName' => 'English (Canadian)' ], + 'en-GB' => [ 'name' => 'English (British)', 'nativeName' => 'English (British)' ], + 'en-NZ' => [ 'name' => 'English (New Zealand)', 'nativeName' => 'English (New Zealand)' ], + 'en-US' => [ 'name' => 'English (US)', 'nativeName' => 'English (US)' ], + 'en-ZA' => [ 'name' => 'English (South African)', 'nativeName' => 'English (South African)' ], + 'eo' => [ 'name' => 'Esperanto', 'nativeName' => 'Esperanto' ], + 'es' => [ 'name' => 'Spanish', 'nativeName' => 'Español' ], + 'es-AR' => [ 'name' => 'Spanish (Argentina)', 'nativeName' => 'Español (de Argentina)' ], + 'es-CL' => [ 'name' => 'Spanish (Chile)', 'nativeName' => 'Español (de Chile)' ], + 'es-ES' => [ 'name' => 'Spanish (Spain)', 'nativeName' => 'Español (de España)' ], + 'es-MX' => [ 'name' => 'Spanish (Mexico)', 'nativeName' => 'Español (de México)' ], + 'et' => [ 'name' => 'Estonian', 'nativeName' => 'Eesti keel' ], + 'eu' => [ 'name' => 'Basque', 'nativeName' => 'Euskara' ], + 'fa' => [ 'name' => 'Persian', 'nativeName' => 'فارسی' , 'orientation' => 'rtl' ], + 'fi' => [ 'name' => 'Finnish', 'nativeName' => 'Suomi' ], + 'fj-FJ' => [ 'name' => 'Fijian', 'nativeName' => 'Vosa vaka-Viti' ], + 'fr' => [ 'name' => 'French', 'nativeName' => 'Français' ], + 'fr-CA' => [ 'name' => 'French (Canada)', 'nativeName' => 'Français (Canada)' ], + 'fr-FR' => [ 'name' => 'French (France)', 'nativeName' => 'Français (France)' ], + 'fur' => [ 'name' => 'Friulian', 'nativeName' => 'Furlan' ], + 'fur-IT' => [ 'name' => 'Friulian', 'nativeName' => 'Furlan' ], + 'fy' => [ 'name' => 'Frisian', 'nativeName' => 'Frysk' ], + 'fy-NL' => [ 'name' => 'Frisian', 'nativeName' => 'Frysk' ], + 'ga' => [ 'name' => 'Irish', 'nativeName' => 'Gaeilge' ], + 'ga-IE' => [ 'name' => 'Irish (Ireland)', 'nativeName' => 'Gaeilge (Éire)' ], + 'gd' => [ 'name' => 'Gaelic (Scotland)', 'nativeName' => 'Gàidhlig' ], + 'gl' => [ 'name' => 'Galician', 'nativeName' => 'Galego' ], + 'gu' => [ 'name' => 'Gujarati', 'nativeName' => 'ગુજરાતી' ], + 'gu-IN' => [ 'name' => 'Gujarati', 'nativeName' => 'ગુજરાતી' ], + 'he' => [ 'name' => 'Hebrew', 'nativeName' => 'עברית', 'orientation' => 'rtl' ], + 'hi' => [ 'name' => 'Hindi', 'nativeName' => 'हिन्दी' ], + 'hi-IN' => [ 'name' => 'Hindi (India)', 'nativeName' => 'हिन्दी (भारत)' ], + 'hr' => [ 'name' => 'Croatian', 'nativeName' => 'Hrvatski' ], + 'hsb' => [ 'name' => 'Upper Sorbian', 'nativeName' => 'Hornjoserbsce' ], + 'hu' => [ 'name' => 'Hungarian', 'nativeName' => 'Magyar' ], + 'hy' => [ 'name' => 'Armenian', 'nativeName' => 'Հայերեն' ], + 'hy-AM' => [ 'name' => 'Armenian', 'nativeName' => 'Հայերեն' ], + 'id' => [ 'name' => 'Indonesian', 'nativeName' => 'Bahasa Indonesia' ], + 'is' => [ 'name' => 'Icelandic', 'nativeName' => 'íslenska' ], + 'it' => [ 'name' => 'Italian', 'nativeName' => 'Italiano' ], + 'ja' => [ 'name' => 'Japanese', 'nativeName' => '日本語' ], + 'ja-JP' => [ 'name' => 'Japanese', 'nativeName' => '日本語' ], // not iso-639-1 + 'ka' => [ 'name' => 'Georgian', 'nativeName' => 'ქართული' ], + 'kk' => [ 'name' => 'Kazakh', 'nativeName' => 'Қазақ' ], + 'km' => [ 'name' => 'Khmer', 'nativeName' => 'Khmer' ], + 'kn' => [ 'name' => 'Kannada', 'nativeName' => 'ಕನ್ನಡ' ], + 'ko' => [ 'name' => 'Korean', 'nativeName' => '한국어' ], + 'ku' => [ 'name' => 'Kurdish', 'nativeName' => 'Kurdî' ], + 'la' => [ 'name' => 'Latin', 'nativeName' => 'Latina' ], + 'lb' => [ 'name' => 'Luxembourgish', 'nativeName' => 'Lëtzebuergesch' ], + 'lg' => [ 'name' => 'Luganda', 'nativeName' => 'Luganda' ], + 'lo' => [ 'name' => 'Lao', 'nativeName' => 'Lao' ], + 'lt' => [ 'name' => 'Lithuanian', 'nativeName' => 'Lietuvių' ], + 'lv' => [ 'name' => 'Latvian', 'nativeName' => 'Latviešu' ], + 'mai' => [ 'name' => 'Maithili', 'nativeName' => 'मैथिली মৈথিলী' ], + 'mg' => [ 'name' => 'Malagasy', 'nativeName' => 'Malagasy' ], + 'mi' => [ 'name' => 'Maori (Aotearoa)', 'nativeName' => 'Māori (Aotearoa)' ], + 'mk' => [ 'name' => 'Macedonian', 'nativeName' => 'Македонски' ], + 'ml' => [ 'name' => 'Malayalam', 'nativeName' => 'മലയാളം' ], + 'mn' => [ 'name' => 'Mongolian', 'nativeName' => 'Монгол' ], + 'mr' => [ 'name' => 'Marathi', 'nativeName' => 'मराठी' ], + 'my' => [ 'name' => 'Myanmar (Burmese)', 'nativeName' => 'ဗမာी' ], + 'no' => [ 'name' => 'Norwegian', 'nativeName' => 'Norsk' ], + 'nb' => [ 'name' => 'Norwegian', 'nativeName' => 'Norsk' ], + 'nb-NO' => [ 'name' => 'Norwegian (Bokmål)', 'nativeName' => 'Norsk bokmål' ], + 'ne-NP' => [ 'name' => 'Nepali', 'nativeName' => 'नेपाली' ], + 'nn-NO' => [ 'name' => 'Norwegian (Nynorsk)', 'nativeName' => 'Norsk nynorsk' ], + 'nl' => [ 'name' => 'Dutch', 'nativeName' => 'Nederlands' ], + 'nr' => [ 'name' => 'Ndebele, South', 'nativeName' => 'IsiNdebele' ], + 'nso' => [ 'name' => 'Northern Sotho', 'nativeName' => 'Sepedi' ], + 'oc' => [ 'name' => 'Occitan (Lengadocian)', 'nativeName' => 'Occitan (lengadocian)' ], + 'or' => [ 'name' => 'Oriya', 'nativeName' => 'ଓଡ଼ିଆ' ], + 'pa' => [ 'name' => 'Punjabi', 'nativeName' => 'ਪੰਜਾਬੀ' ], + 'pa-IN' => [ 'name' => 'Punjabi', 'nativeName' => 'ਪੰਜਾਬੀ' ], + 'pl' => [ 'name' => 'Polish', 'nativeName' => 'Polski' ], + 'pt' => [ 'name' => 'Portuguese', 'nativeName' => 'Português' ], + 'pt-BR' => [ 'name' => 'Portuguese (Brazilian)', 'nativeName' => 'Português (do Brasil)' ], + 'pt-PT' => [ 'name' => 'Portuguese (Portugal)', 'nativeName' => 'Português (Europeu)' ], + 'ro' => [ 'name' => 'Romanian', 'nativeName' => 'Română' ], + 'rm' => [ 'name' => 'Romansh', 'nativeName' => 'Rumantsch' ], + 'ru' => [ 'name' => 'Russian', 'nativeName' => 'Русский' ], + 'rw' => [ 'name' => 'Kinyarwanda', 'nativeName' => 'Ikinyarwanda' ], + 'si' => [ 'name' => 'Sinhala', 'nativeName' => 'සිංහල' ], + 'sk' => [ 'name' => 'Slovak', 'nativeName' => 'Slovenčina' ], + 'sl' => [ 'name' => 'Slovenian', 'nativeName' => 'Slovensko' ], + 'son' => [ 'name' => 'Songhai', 'nativeName' => 'Soŋay' ], + 'sq' => [ 'name' => 'Albanian', 'nativeName' => 'Shqip' ], + 'sr' => [ 'name' => 'Serbian', 'nativeName' => 'Српски' ], + 'sr-Latn' => [ 'name' => 'Serbian', 'nativeName' => 'Srpski' ], // follows RFC 4646 + 'ss' => [ 'name' => 'Siswati', 'nativeName' => 'siSwati' ], + 'st' => [ 'name' => 'Southern Sotho', 'nativeName' => 'Sesotho' ], + 'sv' => [ 'name' => 'Swedish', 'nativeName' => 'Svenska' ], + 'sv-SE' => [ 'name' => 'Swedish', 'nativeName' => 'Svenska' ], + 'sw' => [ 'name' => 'Swahili', 'nativeName' => 'Swahili' ], + 'ta' => [ 'name' => 'Tamil', 'nativeName' => 'தமிழ்' ], + 'ta-IN' => [ 'name' => 'Tamil (India)', 'nativeName' => 'தமிழ் (இந்தியா)' ], + 'ta-LK' => [ 'name' => 'Tamil (Sri Lanka)', 'nativeName' => 'தமிழ் (இலங்கை)' ], + 'te' => [ 'name' => 'Telugu', 'nativeName' => 'తెలుగు' ], + 'th' => [ 'name' => 'Thai', 'nativeName' => 'ไทย' ], + 'tlh' => [ 'name' => 'Klingon', 'nativeName' => 'Klingon' ], + 'tn' => [ 'name' => 'Tswana', 'nativeName' => 'Setswana' ], + 'tr' => [ 'name' => 'Turkish', 'nativeName' => 'Türkçe' ], + 'ts' => [ 'name' => 'Tsonga', 'nativeName' => 'Xitsonga' ], + 'tt' => [ 'name' => 'Tatar', 'nativeName' => 'Tatarça' ], + 'tt-RU' => [ 'name' => 'Tatar', 'nativeName' => 'Tatarça' ], + 'uk' => [ 'name' => 'Ukrainian', 'nativeName' => 'Українська' ], + 'ur' => [ 'name' => 'Urdu', 'nativeName' => 'اُردو', 'orientation' => 'rtl' ], + 've' => [ 'name' => 'Venda', 'nativeName' => 'Tshivenḓa' ], + 'vi' => [ 'name' => 'Vietnamese', 'nativeName' => 'Tiếng Việt' ], + 'wo' => [ 'name' => 'Wolof', 'nativeName' => 'Wolof' ], + 'xh' => [ 'name' => 'Xhosa', 'nativeName' => 'isiXhosa' ], + 'zh' => [ 'name' => 'Chinese (Simplified)', 'nativeName' => '中文 (简体)' ], + 'zh-CN' => [ 'name' => 'Chinese (Simplified)', 'nativeName' => '中文 (简体)' ], + 'zh-TW' => [ 'name' => 'Chinese (Traditional)', 'nativeName' => '正體中文 (繁體)' ], + 'zu' => [ 'name' => 'Zulu', 'nativeName' => 'isiZulu' ] + ]; + + /** + * @param string $code + * @return string|false + */ + public static function getName($code) + { + return static::get($code, 'name'); + } + + /** + * @param string $code + * @return string|false + */ + public static function getNativeName($code) + { + if (isset(static::$codes[$code])) { + return static::get($code, 'nativeName'); + } + + if (preg_match('/[a-zA-Z]{2}-[a-zA-Z]{2}/', $code)) { + return static::get(substr($code, 0, 2), 'nativeName') . ' (' . substr($code, -2) . ')'; + } + + return $code; + } + + /** + * @param string $code + * @return string + */ + public static function getOrientation($code) + { + return static::$codes[$code]['orientation'] ?? 'ltr'; + } + + /** + * @param string $code + * @return bool + */ + public static function isRtl($code) + { + return static::getOrientation($code) === 'rtl'; + } + + /** + * @param array $keys + * @return array + */ + public static function getNames(array $keys) + { + $results = []; + foreach ($keys as $key) { + if (isset(static::$codes[$key])) { + $results[$key] = static::$codes[$key]; + } + } + return $results; + } + + /** + * @param string $code + * @param string $type + * @return string|false + */ + public static function get($code, $type) + { + return static::$codes[$code][$type] ?? false; + } + + /** + * @param bool $native + * @return array + */ + public static function getList($native = true) + { + $list = []; + foreach (static::$codes as $key => $names) { + $list[$key] = $native ? $names['nativeName'] : $names['name']; + } + + return $list; + } +} diff --git a/system/src/Grav/Common/Markdown/Parsedown.php b/system/src/Grav/Common/Markdown/Parsedown.php new file mode 100644 index 0000000..bdc5bdc --- /dev/null +++ b/system/src/Grav/Common/Markdown/Parsedown.php @@ -0,0 +1,42 @@ + $defaults]; + } + $excerpts = new Excerpts($excerpts, $defaults); + user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED); + } + + $this->init($excerpts, $defaults); + } +} diff --git a/system/src/Grav/Common/Markdown/ParsedownExtra.php b/system/src/Grav/Common/Markdown/ParsedownExtra.php new file mode 100644 index 0000000..b8e760e --- /dev/null +++ b/system/src/Grav/Common/Markdown/ParsedownExtra.php @@ -0,0 +1,46 @@ + $defaults]; + } + $excerpts = new Excerpts($excerpts, $defaults); + user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED); + } + + parent::__construct(); + + $this->init($excerpts, $defaults); + } +} diff --git a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php new file mode 100644 index 0000000..7c2b0d6 --- /dev/null +++ b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php @@ -0,0 +1,302 @@ + $defaults]; + } + $this->excerpts = new Excerpts($excerpts, $defaults); + user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use ->init(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED); + } else { + $this->excerpts = $excerpts; + } + + $this->BlockTypes['{'][] = 'TwigTag'; + $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot']; + + $defaults = $this->excerpts->getConfig(); + + if (isset($defaults['markdown']['auto_line_breaks'])) { + $this->setBreaksEnabled($defaults['markdown']['auto_line_breaks']); + } + if (isset($defaults['markdown']['auto_url_links'])) { + $this->setUrlsLinked($defaults['markdown']['auto_url_links']); + } + if (isset($defaults['markdown']['escape_markup'])) { + $this->setMarkupEscaped($defaults['markdown']['escape_markup']); + } + if (isset($defaults['markdown']['special_chars'])) { + $this->setSpecialChars($defaults['markdown']['special_chars']); + } + + $this->excerpts->fireInitializedEvent($this); + } + + /** + * @return Excerpts + */ + public function getExcerpts() + { + return $this->excerpts; + } + + /** + * Be able to define a new Block type or override an existing one + * + * @param string $type + * @param string $tag + * @param bool $continuable + * @param bool $completable + * @param int|null $index + * @return void + */ + public function addBlockType($type, $tag, $continuable = false, $completable = false, $index = null) + { + $block = &$this->unmarkedBlockTypes; + if ($type) { + if (!isset($this->BlockTypes[$type])) { + $this->BlockTypes[$type] = []; + } + $block = &$this->BlockTypes[$type]; + } + + if (null === $index) { + $block[] = $tag; + } else { + array_splice($block, $index, 0, [$tag]); + } + + if ($continuable) { + $this->continuable_blocks[] = $tag; + } + if ($completable) { + $this->completable_blocks[] = $tag; + } + } + + /** + * Be able to define a new Inline type or override an existing one + * + * @param string $type + * @param string $tag + * @param int|null $index + * @return void + */ + public function addInlineType($type, $tag, $index = null) + { + if (null === $index || !isset($this->InlineTypes[$type])) { + $this->InlineTypes[$type] [] = $tag; + } else { + array_splice($this->InlineTypes[$type], $index, 0, [$tag]); + } + + if (strpos($this->inlineMarkerList, $type) === false) { + $this->inlineMarkerList .= $type; + } + } + + /** + * Overrides the default behavior to allow for plugin-provided blocks to be continuable + * + * @param string $Type + * @return bool + */ + protected function isBlockContinuable($Type) + { + $continuable = in_array($Type, $this->continuable_blocks, true) + || method_exists($this, 'block' . $Type . 'Continue'); + + return $continuable; + } + + /** + * Overrides the default behavior to allow for plugin-provided blocks to be completable + * + * @param string $Type + * @return bool + */ + protected function isBlockCompletable($Type) + { + $completable = in_array($Type, $this->completable_blocks, true) + || method_exists($this, 'block' . $Type . 'Complete'); + + return $completable; + } + + + /** + * Make the element function publicly accessible, Medium uses this to render from Twig + * + * @param array $Element + * @return string markup + */ + public function elementToHtml(array $Element) + { + return $this->element($Element); + } + + /** + * Setter for special chars + * + * @param array $special_chars + * @return $this + */ + public function setSpecialChars($special_chars) + { + $this->special_chars = $special_chars; + + return $this; + } + + /** + * Ensure Twig tags are treated as block level items with no

tags + * + * @param array $line + * @return array|null + */ + protected function blockTwigTag($line) + { + if (preg_match('/(?:{{|{%|{#)(.*)(?:}}|%}|#})/', $line['body'], $matches)) { + return ['markup' => $line['body']]; + } + + return null; + } + + /** + * @param array $excerpt + * @return array|null + */ + protected function inlineSpecialCharacter($excerpt) + { + if ($excerpt['text'][0] === '&' && !preg_match('/^&#?\w+;/', $excerpt['text'])) { + return [ + 'markup' => '&', + 'extent' => 1, + ]; + } + + if (isset($this->special_chars[$excerpt['text'][0]])) { + return [ + 'markup' => '&' . $this->special_chars[$excerpt['text'][0]] . ';', + 'extent' => 1, + ]; + } + + return null; + } + + /** + * @param array $excerpt + * @return array + */ + protected function inlineImage($excerpt) + { + if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) { + $excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']); + $excerpt = parent::inlineImage($excerpt); + $excerpt['element']['attributes']['src'] = $matches[1]; + $excerpt['extent'] = $excerpt['extent'] + strlen($matches[1]) - 1; + + return $excerpt; + } + + $excerpt['type'] = 'image'; + $excerpt = parent::inlineImage($excerpt); + + // if this is an image process it + if (isset($excerpt['element']['attributes']['src'])) { + $excerpt = $this->excerpts->processImageExcerpt($excerpt); + } + + return $excerpt; + } + + /** + * @param array $excerpt + * @return array + */ + protected function inlineLink($excerpt) + { + $type = $excerpt['type'] ?? 'link'; + + // do some trickery to get around Parsedown requirement for valid URL if its Twig in there + if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) { + $excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']); + $excerpt = parent::inlineLink($excerpt); + $excerpt['element']['attributes']['href'] = $matches[1]; + $excerpt['extent'] = $excerpt['extent'] + strlen($matches[1]) - 1; + + return $excerpt; + } + + $excerpt = parent::inlineLink($excerpt); + + // if this is a link + if (isset($excerpt['element']['attributes']['href'])) { + $excerpt = $this->excerpts->processLinkExcerpt($excerpt, $type); + } + + return $excerpt; + } + + /** + * For extending this class via plugins + * + * @param string $method + * @param array $args + * @return mixed|null + */ + public function __call($method, $args) + { + if (isset($this->{$method}) === true) { + $func = $this->{$method}; + + return call_user_func_array($func, $args); + } + + return null; + } +} diff --git a/system/src/Grav/Common/Media/Interfaces/AudioMediaInterface.php b/system/src/Grav/Common/Media/Interfaces/AudioMediaInterface.php new file mode 100644 index 0000000..d9c69d2 --- /dev/null +++ b/system/src/Grav/Common/Media/Interfaces/AudioMediaInterface.php @@ -0,0 +1,25 @@ +get('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string|null $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function get($name, $default = null, $separator = null); + + /** + * Set value by using dot notation for nested arrays/objects. + * + * @example $data->set('this.is.my.nested.variable', $value); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value New value. + * @param string|null $separator Separator, defaults to '.' + * @return $this + */ + public function set($name, $value, $separator = null); +} diff --git a/system/src/Grav/Common/Media/Interfaces/MediaPlayerInterface.php b/system/src/Grav/Common/Media/Interfaces/MediaPlayerInterface.php new file mode 100644 index 0000000..9307915 --- /dev/null +++ b/system/src/Grav/Common/Media/Interfaces/MediaPlayerInterface.php @@ -0,0 +1,56 @@ + 'user://pages/media']; // Settings from the form field. + * $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings); + * $media->copyUploadedFile($uploadedFile, $filename); + + * @param UploadedFileInterface $uploadedFile + * @param string|null $filename + * @param array|null $settings + * @return string + * @throws RuntimeException + */ + public function checkUploadedFile(UploadedFileInterface $uploadedFile, string $filename = null, array $settings = null): string; + + /** + * Copy uploaded file to the media collection. + * + * WARNING: Always check uploaded file before copying it! + * + * @example + * $filename = null; // Override filename if needed (ignored if randomizing filenames). + * $settings = ['destination' => 'user://pages/media']; // Settings from the form field. + * $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings); + * $media->copyUploadedFile($uploadedFile, $filename); + * + * @param UploadedFileInterface $uploadedFile + * @param string $filename + * @param array|null $settings + * @return void + * @throws RuntimeException + */ + public function copyUploadedFile(UploadedFileInterface $uploadedFile, string $filename, array $settings = null): void; + + /** + * Delete real file from the media collection. + * + * @param string $filename + * @param array|null $settings + * @return void + */ + public function deleteFile(string $filename, array $settings = null): void; + + /** + * Rename file inside the media collection. + * + * @param string $from + * @param string $to + * @param array|null $settings + */ + public function renameFile(string $from, string $to, array $settings = null): void; +} diff --git a/system/src/Grav/Common/Media/Interfaces/VideoMediaInterface.php b/system/src/Grav/Common/Media/Interfaces/VideoMediaInterface.php new file mode 100644 index 0000000..ff0655a --- /dev/null +++ b/system/src/Grav/Common/Media/Interfaces/VideoMediaInterface.php @@ -0,0 +1,32 @@ +attributes['controlsList'] = $controlsList; + + return $this; + } + + /** + * Parsedown element for source display mode + * + * @param array $attributes + * @param bool $reset + * @return array + */ + protected function sourceParsedownElement(array $attributes, $reset = true) + { + $location = $this->url($reset); + + return [ + 'name' => 'audio', + 'rawHtml' => 'Your browser does not support the audio tag.', + 'attributes' => $attributes + ]; + } +} diff --git a/system/src/Grav/Common/Media/Traits/ImageLoadingTrait.php b/system/src/Grav/Common/Media/Traits/ImageLoadingTrait.php new file mode 100644 index 0000000..c023763 --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/ImageLoadingTrait.php @@ -0,0 +1,37 @@ +get('system.images.defaults.loading', 'auto'); + } + if ($value && $value !== 'auto') { + $this->attributes['loading'] = $value; + } + + return $this; + } +} diff --git a/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php new file mode 100644 index 0000000..f6e2222 --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php @@ -0,0 +1,428 @@ + [0, 1], + 'forceResize' => [0, 1], + 'cropResize' => [0, 1], + 'crop' => [0, 1, 2, 3], + 'zoomCrop' => [0, 1] + ]; + + /** @var string */ + protected $sizes = '100vw'; + + + /** + * Allows the ability to override the image's pretty name stored in cache + * + * @param string $name + */ + public function setImagePrettyName($name) + { + $this->set('prettyname', $name); + if ($this->image) { + $this->image->setPrettyName($name); + } + } + + /** + * @return string + */ + public function getImagePrettyName() + { + if ($this->get('prettyname')) { + return $this->get('prettyname'); + } + + $basename = $this->get('basename'); + if (preg_match('/[a-z0-9]{40}-(.*)/', $basename, $matches)) { + $basename = $matches[1]; + } + return $basename; + } + + /** + * Simply processes with no extra methods. Useful for triggering events. + * + * @return $this + */ + public function cache() + { + if (!$this->image) { + $this->image(); + } + + return $this; + } + + /** + * Generate alternative image widths, using either an array of integers, or + * a min width, a max width, and a step parameter to fill out the necessary + * widths. Existing image alternatives won't be overwritten. + * + * @param int|int[] $min_width + * @param int $max_width + * @param int $step + * @return $this + */ + public function derivatives($min_width, $max_width = 2500, $step = 200) + { + if (!empty($this->alternatives)) { + $max = max(array_keys($this->alternatives)); + $base = $this->alternatives[$max]; + } else { + $base = $this; + } + + $widths = []; + + if (func_num_args() === 1) { + foreach ((array) func_get_arg(0) as $width) { + if ($width < $base->get('width')) { + $widths[] = $width; + } + } + } else { + $max_width = min($max_width, $base->get('width')); + + for ($width = $min_width; $width < $max_width; $width += $step) { + $widths[] = $width; + } + } + + foreach ($widths as $width) { + // Only generate image alternatives that don't already exist + if (array_key_exists((int) $width, $this->alternatives)) { + continue; + } + + $derivative = MediumFactory::fromFile($base->get('filepath')); + + // It's possible that MediumFactory::fromFile returns null if the + // original image file no longer exists and this class instance was + // retrieved from the page cache + if (null !== $derivative) { + $index = 2; + $alt_widths = array_keys($this->alternatives); + sort($alt_widths); + + foreach ($alt_widths as $i => $key) { + if ($width > $key) { + $index += max($i, 1); + } + } + + $basename = preg_replace('/(@\d+x)?$/', "@{$width}w", $base->get('basename'), 1); + $derivative->setImagePrettyName($basename); + + $ratio = $base->get('width') / $width; + $height = $derivative->get('height') / $ratio; + + $derivative->resize($width, $height); + $derivative->set('width', $width); + $derivative->set('height', $height); + + $this->addAlternative($ratio, $derivative); + } + } + + return $this; + } + + /** + * Clear out the alternatives. + */ + public function clearAlternatives() + { + $this->alternatives = []; + } + + /** + * Sets or gets the quality of the image + * + * @param int|null $quality 0-100 quality + * @return int|$this + */ + public function quality($quality = null) + { + if ($quality) { + if (!$this->image) { + $this->image(); + } + + $this->quality = $quality; + + return $this; + } + + return $this->quality; + } + + /** + * Sets image output format. + * + * @param string $format + * @return $this + */ + public function format($format) + { + if (!$this->image) { + $this->image(); + } + + $this->format = $format; + + return $this; + } + + /** + * Set or get sizes parameter for srcset media action + * + * @param string|null $sizes + * @return string + */ + public function sizes($sizes = null) + { + if ($sizes) { + $this->sizes = $sizes; + + return $this; + } + + return empty($this->sizes) ? '100vw' : $this->sizes; + } + + /** + * Allows to set the width attribute from Markdown or Twig + * Examples: ![Example](myimg.png?width=200&height=400) + * ![Example](myimg.png?resize=100,200&width=100&height=200) + * ![Example](myimg.png?width=auto&height=auto) + * ![Example](myimg.png?width&height) + * {{ page.media['myimg.png'].width().height().html }} + * {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }} + * + * @param string|int $value A value or 'auto' or empty to use the width of the image + * @return $this + */ + public function width($value = 'auto') + { + if (!$value || $value === 'auto') { + $this->attributes['width'] = $this->get('width'); + } else { + $this->attributes['width'] = $value; + } + + return $this; + } + + /** + * Allows to set the height attribute from Markdown or Twig + * Examples: ![Example](myimg.png?width=200&height=400) + * ![Example](myimg.png?resize=100,200&width=100&height=200) + * ![Example](myimg.png?width=auto&height=auto) + * ![Example](myimg.png?width&height) + * {{ page.media['myimg.png'].width().height().html }} + * {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }} + * + * @param string|int $value A value or 'auto' or empty to use the height of the image + * @return $this + */ + public function height($value = 'auto') + { + if (!$value || $value === 'auto') { + $this->attributes['height'] = $this->get('height'); + } else { + $this->attributes['height'] = $value; + } + + return $this; + } + + /** + * Filter image by using user defined filter parameters. + * + * @param string $filter Filter to be used. + * @return $this + */ + public function filter($filter = 'image.filters.default') + { + $filters = (array) $this->get($filter, []); + foreach ($filters as $params) { + $params = (array) $params; + $method = array_shift($params); + $this->__call($method, $params); + } + + return $this; + } + + /** + * Return the image higher quality version + * + * @return ImageMediaInterface|$this the alternative version with higher quality + */ + public function higherQualityAlternative() + { + if ($this->alternatives) { + /** @var ImageMedium $max */ + $max = reset($this->alternatives); + /** @var ImageMedium $alternative */ + foreach ($this->alternatives as $alternative) { + if ($alternative->quality() > $max->quality()) { + $max = $alternative; + } + } + + return $max; + } + + return $this; + } + + /** + * Gets medium image, resets image manipulation operations. + * + * @return $this + */ + protected function image() + { + $locator = Grav::instance()['locator']; + + // Use existing cache folder or if it doesn't exist, create it. + $cacheDir = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true); + + // Make sure we free previous image. + unset($this->image); + + /** @var MediaCollectionInterface $media */ + $media = $this->get('media'); + if ($media && method_exists($media, 'getImageFileObject')) { + $this->image = $media->getImageFileObject($this); + } else { + $this->image = ImageFile::open($this->get('filepath')); + } + + $this->image + ->setCacheDir($cacheDir) + ->setActualCacheDir($cacheDir) + ->setPrettyName($this->getImagePrettyName()); + + // Fix orientation if enabled + $config = Grav::instance()['config']; + if ($config->get('system.images.auto_fix_orientation', false) && + extension_loaded('exif') && function_exists('exif_read_data')) { + $this->image->fixOrientation(); + } + + // Set CLS configuration + $this->auto_sizes = $config->get('system.images.cls.auto_sizes', false); + $this->aspect_ratio = $config->get('system.images.cls.aspect_ratio', false); + $this->retina_scale = $config->get('system.images.cls.retina_scale', 1); + + $this->watermark = $config->get('system.images.watermark.watermark_all', false); + + return $this; + } + + /** + * Save the image with cache. + * + * @return string + */ + protected function saveImage() + { + if (!$this->image) { + return parent::path(false); + } + + $this->filter(); + + if (isset($this->result)) { + return $this->result; + } + + if ($this->format === 'guess') { + $extension = strtolower($this->get('extension')); + $this->format($extension); + } + + if (!$this->debug_watermarked && $this->get('debug')) { + $ratio = $this->get('ratio'); + if (!$ratio) { + $ratio = 1; + } + + $locator = Grav::instance()['locator']; + $overlay = $locator->findResource("system://assets/responsive-overlays/{$ratio}x.png") ?: $locator->findResource('system://assets/responsive-overlays/unknown.png'); + $this->image->merge(ImageFile::open($overlay)); + } + + if ($this->watermark) { + $this->watermark(); + } + + return $this->image->cacheFile($this->format, $this->quality, false, [$this->get('width'), $this->get('height'), $this->get('modified')]); + } +} diff --git a/system/src/Grav/Common/Media/Traits/MediaFileTrait.php b/system/src/Grav/Common/Media/Traits/MediaFileTrait.php new file mode 100644 index 0000000..7198133 --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/MediaFileTrait.php @@ -0,0 +1,139 @@ +path(false); + + return file_exists($path); + } + + /** + * Get file modification time for the medium. + * + * @return int|null + */ + public function modified() + { + $path = $this->path(false); + if (!file_exists($path)) { + return null; + } + + return filemtime($path) ?: null; + } + + /** + * Get size of the medium. + * + * @return int + */ + public function size() + { + $path = $this->path(false); + if (!file_exists($path)) { + return 0; + } + + return filesize($path) ?: 0; + } + + /** + * Return PATH to file. + * + * @param bool $reset + * @return string path to file + */ + public function path($reset = true) + { + if ($reset) { + $this->reset(); + } + + return $this->get('url') ?? $this->get('filepath'); + } + + /** + * Return the relative path to file + * + * @param bool $reset + * @return string + */ + public function relativePath($reset = true) + { + if ($reset) { + $this->reset(); + } + + $path = $this->path(false); + $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $path) ?: $path; + + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + if ($locator->isStream($output)) { + $output = (string)($locator->findResource($output, false) ?: $locator->findResource($output, false, true)); + } + + return $output; + } + + /** + * Return URL to file. + * + * @param bool $reset + * @return string + */ + public function url($reset = true) + { + $url = $this->get('url'); + if ($url) { + return $url; + } + + $path = $this->relativePath($reset); + + return trim($this->getGrav()['base_url'] . '/' . $this->urlQuerystring($path), '\\'); + } + + /** + * Get the URL with full querystring + * + * @param string $url + * @return string + */ + abstract public function urlQuerystring($url); + + /** + * Reset medium. + * + * @return $this + */ + abstract public function reset(); + + /** + * @return Grav + */ + abstract protected function getGrav(): Grav; +} diff --git a/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php b/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php new file mode 100644 index 0000000..3c2a38b --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php @@ -0,0 +1,609 @@ +getItems()); + } + + /** + * Set querystring to file modification timestamp (or value provided as a parameter). + * + * @param string|int|null $timestamp + * @return $this + */ + public function setTimestamp($timestamp = null) + { + if (null !== $timestamp) { + $this->timestamp = (string)($timestamp); + } elseif ($this instanceof MediaFileInterface) { + $this->timestamp = (string)$this->modified(); + } else { + $this->timestamp = ''; + } + + return $this; + } + + /** + * Returns an array containing just the metadata + * + * @return array + */ + public function metadata() + { + return $this->metadata; + } + + /** + * Add meta file for the medium. + * + * @param string $filepath + */ + abstract public function addMetaFile($filepath); + + /** + * Add alternative Medium to this Medium. + * + * @param int|float $ratio + * @param MediaObjectInterface $alternative + */ + public function addAlternative($ratio, MediaObjectInterface $alternative) + { + if (!is_numeric($ratio) || $ratio === 0) { + return; + } + + $alternative->set('ratio', $ratio); + $width = $alternative->get('width'); + + $this->alternatives[$width] = $alternative; + } + + /** + * Return string representation of the object (html). + * + * @return string + */ + abstract public function __toString(); + + /** + * Get/set querystring for the file's url + * + * @param string|null $querystring + * @param bool $withQuestionmark + * @return string + */ + public function querystring($querystring = null, $withQuestionmark = true) + { + if (null !== $querystring) { + $this->medium_querystring[] = ltrim($querystring, '?&'); + foreach ($this->alternatives as $alt) { + $alt->querystring($querystring, $withQuestionmark); + } + } + + if (empty($this->medium_querystring)) { + return ''; + } + + // join the strings + $querystring = implode('&', $this->medium_querystring); + // explode all strings + $query_parts = explode('&', $querystring); + // Join them again now ensure the elements are unique + $querystring = implode('&', array_unique($query_parts)); + + return $withQuestionmark ? ('?' . $querystring) : $querystring; + } + + /** + * Get the URL with full querystring + * + * @param string $url + * @return string + */ + public function urlQuerystring($url) + { + $querystring = $this->querystring(); + if (isset($this->timestamp) && !Utils::contains($querystring, $this->timestamp)) { + $querystring = empty($querystring) ? ('?' . $this->timestamp) : ($querystring . '&' . $this->timestamp); + } + + return ltrim($url . $querystring . $this->urlHash(), '/'); + } + + /** + * Get/set hash for the file's url + * + * @param string|null $hash + * @param bool $withHash + * @return string + */ + public function urlHash($hash = null, $withHash = true) + { + if ($hash) { + $this->set('urlHash', ltrim($hash, '#')); + } + + $hash = $this->get('urlHash', ''); + + return $withHash && !empty($hash) ? '#' . $hash : $hash; + } + + /** + * Get an element (is array) that can be rendered by the Parsedown engine + * + * @param string|null $title + * @param string|null $alt + * @param string|null $class + * @param string|null $id + * @param bool $reset + * @return array + */ + public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true) + { + $attributes = $this->attributes; + $items = $this->getItems(); + + $style = ''; + foreach ($this->styleAttributes as $key => $value) { + if (is_numeric($key)) { // Special case for inline style attributes, refer to style() method + $style .= $value; + } else { + $style .= $key . ': ' . $value . ';'; + } + } + if ($style) { + $attributes['style'] = $style; + } + + if (empty($attributes['title'])) { + if (!empty($title)) { + $attributes['title'] = $title; + } elseif (!empty($items['title'])) { + $attributes['title'] = $items['title']; + } + } + + if (empty($attributes['alt'])) { + if (!empty($alt)) { + $attributes['alt'] = $alt; + } elseif (!empty($items['alt'])) { + $attributes['alt'] = $items['alt']; + } elseif (!empty($items['alt_text'])) { + $attributes['alt'] = $items['alt_text']; + } else { + $attributes['alt'] = ''; + } + } + + if (empty($attributes['class'])) { + if (!empty($class)) { + $attributes['class'] = $class; + } elseif (!empty($items['class'])) { + $attributes['class'] = $items['class']; + } + } + + if (empty($attributes['id'])) { + if (!empty($id)) { + $attributes['id'] = $id; + } elseif (!empty($items['id'])) { + $attributes['id'] = $items['id']; + } + } + + switch ($this->mode) { + case 'text': + $element = $this->textParsedownElement($attributes, false); + break; + case 'thumbnail': + $thumbnail = $this->getThumbnail(); + $element = $thumbnail ? $thumbnail->sourceParsedownElement($attributes, false) : []; + break; + case 'source': + $element = $this->sourceParsedownElement($attributes, false); + break; + default: + $element = []; + } + + if ($reset) { + $this->reset(); + } + + $this->display('source'); + + return $element; + } + + /** + * Reset medium. + * + * @return $this + */ + public function reset() + { + $this->attributes = []; + + return $this; + } + + /** + * Add custom attribute to medium. + * + * @param string $attribute + * @param string $value + * @return $this + */ + public function attribute($attribute = null, $value = '') + { + if (!empty($attribute)) { + $this->attributes[$attribute] = $value; + } + return $this; + } + + /** + * Switch display mode. + * + * @param string $mode + * + * @return MediaObjectInterface|null + */ + public function display($mode = 'source') + { + if ($this->mode === $mode) { + return $this; + } + + $this->mode = $mode; + if ($mode === 'thumbnail') { + $thumbnail = $this->getThumbnail(); + + return $thumbnail ? $thumbnail->reset() : null; + } + + return $this->reset(); + } + + /** + * Helper method to determine if this media item has a thumbnail or not + * + * @param string $type; + * @return bool + */ + public function thumbnailExists($type = 'page') + { + $thumbs = $this->get('thumbnails'); + + return isset($thumbs[$type]); + } + + /** + * Switch thumbnail. + * + * @param string $type + * @return $this + */ + public function thumbnail($type = 'auto') + { + if ($type !== 'auto' && !in_array($type, $this->thumbnailTypes, true)) { + return $this; + } + + if ($this->thumbnailType !== $type) { + $this->_thumbnail = null; + } + + $this->thumbnailType = $type; + + return $this; + } + + /** + * Return URL to file. + * + * @param bool $reset + * @return string + */ + abstract public function url($reset = true); + + /** + * Turn the current Medium into a Link + * + * @param bool $reset + * @param array $attributes + * @return MediaLinkInterface + */ + public function link($reset = true, array $attributes = []) + { + if ($this->mode !== 'source') { + $this->display('source'); + } + + foreach ($this->attributes as $key => $value) { + empty($attributes['data-' . $key]) && $attributes['data-' . $key] = $value; + } + + empty($attributes['href']) && $attributes['href'] = $this->url(); + + return $this->createLink($attributes); + } + + /** + * Turn the current Medium into a Link with lightbox enabled + * + * @param int|null $width + * @param int|null $height + * @param bool $reset + * @return MediaLinkInterface + */ + public function lightbox($width = null, $height = null, $reset = true) + { + $attributes = ['rel' => 'lightbox']; + + if ($width && $height) { + $attributes['data-width'] = $width; + $attributes['data-height'] = $height; + } + + return $this->link($reset, $attributes); + } + + /** + * Add a class to the element from Markdown or Twig + * Example: ![Example](myimg.png?classes=float-left) or ![Example](myimg.png?classes=myclass1,myclass2) + * + * @return $this + */ + public function classes() + { + $classes = func_get_args(); + if (!empty($classes)) { + $this->attributes['class'] = implode(',', $classes); + } + + return $this; + } + + /** + * Add an id to the element from Markdown or Twig + * Example: ![Example](myimg.png?id=primary-img) + * + * @param string $id + * @return $this + */ + public function id($id) + { + if (is_string($id)) { + $this->attributes['id'] = trim($id); + } + + return $this; + } + + /** + * Allows to add an inline style attribute from Markdown or Twig + * Example: ![Example](myimg.png?style=float:left) + * + * @param string $style + * @return $this + */ + public function style($style) + { + $this->styleAttributes[] = rtrim($style, ';') . ';'; + + return $this; + } + + /** + * Allow any action to be called on this medium from twig or markdown + * + * @param string $method + * @param array $args + * @return $this + */ + public function __call($method, $args) + { + $count = count($args); + if ($count > 1 || ($count === 1 && !empty($args[0]))) { + $method .= '=' . implode(',', array_map(static function ($a) { + if (is_array($a)) { + $a = '[' . implode(',', $a) . ']'; + } + + return rawurlencode($a); + }, $args)); + } + + if (!empty($method)) { + $this->querystring($this->querystring(null, false) . '&' . $method); + } + + return $this; + } + + /** + * Parsedown element for source display mode + * + * @param array $attributes + * @param bool $reset + * @return array + */ + protected function sourceParsedownElement(array $attributes, $reset = true) + { + return $this->textParsedownElement($attributes, $reset); + } + + /** + * Parsedown element for text display mode + * + * @param array $attributes + * @param bool $reset + * @return array + */ + protected function textParsedownElement(array $attributes, $reset = true) + { + if ($reset) { + $this->reset(); + } + + $text = $attributes['title'] ?? ''; + if ($text === '') { + $text = $attributes['alt'] ?? ''; + if ($text === '') { + $text = $this->get('filename'); + } + } + + return [ + 'name' => 'p', + 'attributes' => $attributes, + 'text' => $text + ]; + } + + /** + * Get the thumbnail Medium object + * + * @return ThumbnailImageMedium|null + */ + protected function getThumbnail() + { + if (null === $this->_thumbnail) { + $types = $this->thumbnailTypes; + + if ($this->thumbnailType !== 'auto') { + array_unshift($types, $this->thumbnailType); + } + + foreach ($types as $type) { + $thumb = $this->get("thumbnails.{$type}", false); + + if ($thumb) { + $thumb = $thumb instanceof ThumbnailImageMedium ? $thumb : $this->createThumbnail($thumb); + $thumb->parent = $this; + $this->_thumbnail = $thumb; + break; + } + } + } + + return $this->_thumbnail; + } + + /** + * Get value by using dot notation for nested arrays/objects. + * + * @example $value = $this->get('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string|null $separator Separator, defaults to '.' + * @return mixed Value. + */ + abstract public function get($name, $default = null, $separator = null); + + /** + * Set value by using dot notation for nested arrays/objects. + * + * @example $data->set('this.is.my.nested.variable', $value); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value New value. + * @param string|null $separator Separator, defaults to '.' + * @return $this + */ + abstract public function set($name, $value, $separator = null); + + /** + * @param string $thumb + */ + abstract protected function createThumbnail($thumb); + + /** + * @param array $attributes + * @return MediaLinkInterface + */ + abstract protected function createLink(array $attributes); + + /** + * @return array + */ + abstract protected function getItems(): array; +} diff --git a/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php b/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php new file mode 100644 index 0000000..66e9f47 --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php @@ -0,0 +1,113 @@ +attributes['controls'] = true; + } else { + unset($this->attributes['controls']); + } + + return $this; + } + + /** + * Allows to set the loop attribute + * + * @param bool $status + * @return $this + */ + public function loop($status = false) + { + if ($status) { + $this->attributes['loop'] = true; + } else { + unset($this->attributes['loop']); + } + + return $this; + } + + /** + * Allows to set the autoplay attribute + * + * @param bool $status + * @return $this + */ + public function autoplay($status = false) + { + if ($status) { + $this->attributes['autoplay'] = true; + } else { + unset($this->attributes['autoplay']); + } + + return $this; + } + + /** + * Allows to set the muted attribute + * + * @param bool $status + * @return $this + */ + public function muted($status = false) + { + if ($status) { + $this->attributes['muted'] = true; + } else { + unset($this->attributes['muted']); + } + + return $this; + } + + /** + * Allows to set the preload behaviour + * + * @param string|null $preload + * @return $this + */ + public function preload($preload = null) + { + $validPreloadAttrs = ['auto', 'metadata', 'none']; + + if (null === $preload) { + unset($this->attributes['preload']); + } elseif (in_array($preload, $validPreloadAttrs, true)) { + $this->attributes['preload'] = $preload; + } + + return $this; + } + + /** + * Reset player. + */ + public function resetPlayer() + { + $this->attributes['controls'] = true; + } +} diff --git a/system/src/Grav/Common/Media/Traits/MediaTrait.php b/system/src/Grav/Common/Media/Traits/MediaTrait.php new file mode 100644 index 0000000..5faba82 --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/MediaTrait.php @@ -0,0 +1,153 @@ +getMediaFolder(); + if (!$folder) { + return null; + } + + if (strpos($folder, '://')) { + return $folder; + } + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $user = $locator->findResource('user://'); + if (strpos($folder, $user) === 0) { + return 'user://' . substr($folder, strlen($user)+1); + } + + return null; + } + + /** + * Gets the associated media collection. + * + * @return MediaCollectionInterface|Media Representation of associated media. + */ + public function getMedia() + { + $media = $this->media; + if (null === $media) { + $cache = $this->getMediaCache(); + $cacheKey = md5('media' . $this->getCacheKey()); + + // Use cached media if possible. + $media = $cache->get($cacheKey); + if (!$media instanceof MediaCollectionInterface) { + $media = new Media($this->getMediaFolder(), $this->getMediaOrder(), $this->_loadMedia); + $cache->set($cacheKey, $media); + } + + $this->media = $media; + } + + return $media; + } + + /** + * Sets the associated media collection. + * + * @param MediaCollectionInterface|Media $media Representation of associated media. + * @return $this + */ + protected function setMedia(MediaCollectionInterface $media) + { + $cache = $this->getMediaCache(); + $cacheKey = md5('media' . $this->getCacheKey()); + $cache->set($cacheKey, $media); + + $this->media = $media; + + return $this; + } + + /** + * @return void + */ + protected function freeMedia() + { + $this->media = null; + } + + /** + * Clear media cache. + * + * @return void + */ + protected function clearMediaCache() + { + $cache = $this->getMediaCache(); + $cacheKey = md5('media' . $this->getCacheKey()); + $cache->delete($cacheKey); + + $this->freeMedia(); + } + + /** + * @return CacheInterface + */ + protected function getMediaCache() + { + /** @var Cache $cache */ + $cache = Grav::instance()['cache']; + + return $cache->getSimpleCache(); + } + + /** + * @return string + */ + abstract protected function getCacheKey(): string; +} diff --git a/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php b/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php new file mode 100644 index 0000000..7a1f55d --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php @@ -0,0 +1,674 @@ + true, // Whether path is in the media collection path itself. + 'avoid_overwriting' => false, // Do not override existing files (adds datetime postfix if conflict). + 'random_name' => false, // True if name needs to be randomized. + 'accept' => ['image/*'], // Accepted mime types or file extensions. + 'limit' => 10, // Maximum number of files. + 'filesize' => null, // Maximum filesize in MB. + 'destination' => null // Destination path, if empty, exception is thrown. + ]; + + /** + * Create Medium from an uploaded file. + * + * @param UploadedFileInterface $uploadedFile + * @param array $params + * @return Medium|null + */ + public function createFromUploadedFile(UploadedFileInterface $uploadedFile, array $params = []) + { + return MediumFactory::fromUploadedFile($uploadedFile, $params); + } + + /** + * Checks that uploaded file meets the requirements. Returns new filename. + * + * @example + * $filename = null; // Override filename if needed (ignored if randomizing filenames). + * $settings = ['destination' => 'user://pages/media']; // Settings from the form field. + * $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings); + * $media->copyUploadedFile($uploadedFile, $filename); + * + * @param UploadedFileInterface $uploadedFile + * @param string|null $filename + * @param array|null $settings + * @return string + * @throws RuntimeException + */ + public function checkUploadedFile(UploadedFileInterface $uploadedFile, string $filename = null, array $settings = null): string + { + // Check if there is an upload error. + switch ($uploadedFile->getError()) { + case UPLOAD_ERR_OK: + break; + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT'), 400); + case UPLOAD_ERR_PARTIAL: + case UPLOAD_ERR_NO_FILE: + if (!$uploadedFile instanceof FormFlashFile) { + throw new RuntimeException($this->translate('PLUGIN_ADMIN.NO_FILES_SENT'), 400); + } + break; + case UPLOAD_ERR_NO_TMP_DIR: + throw new RuntimeException($this->translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR'), 400); + case UPLOAD_ERR_CANT_WRITE: + case UPLOAD_ERR_EXTENSION: + default: + throw new RuntimeException($this->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 400); + } + + $metadata = [ + 'filename' => $uploadedFile->getClientFilename(), + 'mime' => $uploadedFile->getClientMediaType(), + 'size' => $uploadedFile->getSize(), + ]; + + return $this->checkFileMetadata($metadata, $filename, $settings); + } + + /** + * Checks that file metadata meets the requirements. Returns new filename. + * + * @param array $metadata + * @param array|null $settings + * @return string + * @throws RuntimeException + */ + public function checkFileMetadata(array $metadata, string $filename = null, array $settings = null): string + { + // Add the defaults to the settings. + $settings = $this->getUploadSettings($settings); + + // Destination is always needed (but it can be set in defaults). + $self = $settings['self'] ?? false; + if (!isset($settings['destination']) && $self === false) { + throw new RuntimeException($this->translate('PLUGIN_ADMIN.DESTINATION_NOT_SPECIFIED'), 400); + } + + if (null === $filename) { + // If no filename is given, use the filename from the uploaded file (path is not allowed). + $folder = ''; + $filename = $metadata['filename'] ?? ''; + } else { + // If caller sets the filename, we will accept any custom path. + $folder = dirname($filename); + if ($folder === '.') { + $folder = ''; + } + $filename = basename($filename); + } + $extension = pathinfo($filename, PATHINFO_EXTENSION); + + // Decide which filename to use. + if ($settings['random_name']) { + // Generate random filename if asked for. + $filename = mb_strtolower(Utils::generateRandomString(15) . '.' . $extension); + } + + // Handle conflicting filename if needed. + if ($settings['avoid_overwriting']) { + $destination = $settings['destination']; + if ($destination && $this->fileExists($filename, $destination)) { + $filename = date('YmdHis') . '-' . $filename; + } + } + $filepath = $folder . $filename; + + // Check if the filename is allowed. + if (!Utils::checkFilename($filename)) { + throw new RuntimeException( + sprintf($this->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD'), $filepath, $this->translate('PLUGIN_ADMIN.BAD_FILENAME')) + ); + } + + // Check if the file extension is allowed. + $extension = mb_strtolower($extension); + if (!$extension || !$this->getConfig()->get("media.types.{$extension}")) { + // Not a supported type. + throw new RuntimeException($this->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400); + } + + // Calculate maximum file size (from MB). + $filesize = $settings['filesize']; + if ($filesize) { + $max_filesize = $filesize * 1048576; + if ($metadata['size'] > $max_filesize) { + // TODO: use own language string + throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400); + } + } elseif (null === $filesize) { + // Check size against the Grav upload limit. + $grav_limit = Utils::getUploadLimit(); + if ($grav_limit > 0 && $metadata['size'] > $grav_limit) { + throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400); + } + } + + $grav = Grav::instance(); + /** @var MimeTypes $mimeChecker */ + $mimeChecker = $grav['mime']; + + // Handle Accepted file types. Accept can only be mime types (image/png | image/*) or file extensions (.pdf | .jpg) + // Do not trust mime type sent by the browser. + $mime = $metadata['mime'] ?? $mimeChecker->getMimeType($extension); + $validExtensions = $mimeChecker->getExtensions($mime); + if (!in_array($extension, $validExtensions, true)) { + throw new RuntimeException('The mime type does not match to file extension', 400); + } + + $accepted = false; + $errors = []; + foreach ((array)$settings['accept'] as $type) { + // Force acceptance of any file when star notation + if ($type === '*') { + $accepted = true; + break; + } + + $isMime = strstr($type, '/'); + $find = str_replace(['.', '*', '+'], ['\.', '.*', '\+'], $type); + + if ($isMime) { + $match = preg_match('#' . $find . '$#', $mime); + if (!$match) { + // TODO: translate + $errors[] = 'The MIME type "' . $mime . '" for the file "' . $filepath . '" is not an accepted.'; + } else { + $accepted = true; + break; + } + } else { + $match = preg_match('#' . $find . '$#', $filename); + if (!$match) { + // TODO: translate + $errors[] = 'The File Extension for the file "' . $filepath . '" is not an accepted.'; + } else { + $accepted = true; + break; + } + } + } + if (!$accepted) { + throw new RuntimeException(implode('
', $errors), 400); + } + + return $filepath; + } + + /** + * Copy uploaded file to the media collection. + * + * WARNING: Always check uploaded file before copying it! + * + * @example + * $settings = ['destination' => 'user://pages/media']; // Settings from the form field. + * $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings); + * $media->copyUploadedFile($uploadedFile, $filename, $settings); + * + * @param UploadedFileInterface $uploadedFile + * @param string $filename + * @param array|null $settings + * @return void + * @throws RuntimeException + */ + public function copyUploadedFile(UploadedFileInterface $uploadedFile, string $filename, array $settings = null): void + { + // Add the defaults to the settings. + $settings = $this->getUploadSettings($settings); + + $path = $settings['destination'] ?? $this->getPath(); + if (!$path || !$filename) { + throw new RuntimeException($this->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400); + } + + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + + try { + // Clear locator cache to make sure we have up to date information from the filesystem. + $locator->clearCache(); + $this->clearCache(); + + $filesystem = Filesystem::getInstance(false); + + // Calculate path without the retina scaling factor. + $basename = $filesystem->basename($filename); + $pathname = $filesystem->pathname($filename); + + // Get name for the uploaded file. + [$base, $ext,,] = $this->getFileParts($basename); + $name = "{$pathname}{$base}.{$ext}"; + + // Upload file. + if ($uploadedFile instanceof FormFlashFile) { + // FormFlashFile needs some additional logic. + if ($uploadedFile->getError() === \UPLOAD_ERR_OK) { + // Move uploaded file. + $this->doMoveUploadedFile($uploadedFile, $filename, $path); + } elseif (strpos($filename, 'original/') === 0 && !$this->fileExists($filename, $path) && $this->fileExists($basename, $path)) { + // Original image support: override original image if it's the same as the uploaded image. + $this->doCopy($basename, $filename, $path); + } + + // FormFlashFile may also contain metadata. + $metadata = $uploadedFile->getMetaData(); + if ($metadata) { + // TODO: This overrides metadata if used with multiple retina image sizes. + $this->doSaveMetadata(['upload' => $metadata], $name, $path); + } + } else { + // Not a FormFlashFile. + $this->doMoveUploadedFile($uploadedFile, $filename, $path); + } + + // Post-processing: Special content sanitization for SVG. + $mime = Utils::getMimeByFilename($filename); + if (Utils::contains($mime, 'svg', false)) { + $this->doSanitizeSvg($filename, $path); + } + + // Add the new file into the media. + // TODO: This overrides existing media sizes if used with multiple retina image sizes. + $this->doAddUploadedMedium($name, $filename, $path); + + } catch (Exception $e) { + throw new RuntimeException($this->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE') . $e->getMessage(), 400); + } finally { + // Finally clear media cache. + $locator->clearCache(); + $this->clearCache(); + } + } + + /** + * Delete real file from the media collection. + * + * @param string $filename + * @param array|null $settings + * @return void + * @throws RuntimeException + */ + public function deleteFile(string $filename, array $settings = null): void + { + // Add the defaults to the settings. + $settings = $this->getUploadSettings($settings); + $filesystem = Filesystem::getInstance(false); + + // First check for allowed filename. + $basename = $filesystem->basename($filename); + if (!Utils::checkFilename($basename)) { + throw new RuntimeException($this->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ": {$this->translate('PLUGIN_ADMIN.BAD_FILENAME')}: " . $filename, 400); + } + + $path = $settings['destination'] ?? $this->getPath(); + if (!$path) { + return; + } + + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + $locator->clearCache(); + + $pathname = $filesystem->pathname($filename); + + // Get base name of the file. + [$base, $ext,,] = $this->getFileParts($basename); + $name = "{$pathname}{$base}.{$ext}"; + + // Remove file and all all the associated metadata. + $this->doRemove($name, $path); + + // Finally clear media cache. + $locator->clearCache(); + $this->clearCache(); + } + + /** + * Rename file inside the media collection. + * + * @param string $from + * @param string $to + * @param array|null $settings + */ + public function renameFile(string $from, string $to, array $settings = null): void + { + // Add the defaults to the settings. + $settings = $this->getUploadSettings($settings); + $filesystem = Filesystem::getInstance(false); + + $path = $settings['destination'] ?? $this->getPath(); + if (!$path) { + // TODO: translate error message + throw new RuntimeException('Failed to rename file: Bad destination', 400); + } + + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + $locator->clearCache(); + + // Get base name of the file. + $pathname = $filesystem->pathname($from); + + // Remove @2x, @3x and .meta.yaml + [$base, $ext,,] = $this->getFileParts($filesystem->basename($from)); + $from = "{$pathname}{$base}.{$ext}"; + + [$base, $ext,,] = $this->getFileParts($filesystem->basename($to)); + $to = "{$pathname}{$base}.{$ext}"; + + $this->doRename($from, $to, $path); + + // Finally clear media cache. + $locator->clearCache(); + $this->clearCache(); + } + + /** + * Internal logic to move uploaded file. + * + * @param UploadedFileInterface $uploadedFile + * @param string $filename + * @param string $path + */ + protected function doMoveUploadedFile(UploadedFileInterface $uploadedFile, string $filename, string $path): void + { + $filepath = sprintf('%s/%s', $path, $filename); + + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + + // Do not use streams internally. + if ($locator->isStream($filepath)) { + $filepath = (string)$locator->findResource($filepath, true, true); + } + + Folder::create(dirname($filepath)); + + $uploadedFile->moveTo($filepath); + } + + /** + * Get upload settings. + * + * @param array|null $settings Form field specific settings (override). + * @return array + */ + public function getUploadSettings(?array $settings = null): array + { + return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults; + } + + /** + * Internal logic to copy file. + * + * @param string $src + * @param string $dst + * @param string $path + */ + protected function doCopy(string $src, string $dst, string $path): void + { + $src = sprintf('%s/%s', $path, $src); + $dst = sprintf('%s/%s', $path, $dst); + + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + + // Do not use streams internally. + if ($locator->isStream($dst)) { + $dst = (string)$locator->findResource($dst, true, true); + } + + Folder::create(dirname($dst)); + + copy($src, $dst); + } + + /** + * Internal logic to rename file. + * + * @param string $from + * @param string $to + * @param string $path + */ + protected function doRename(string $from, string $to, string $path): void + { + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + + $fromPath = $path . '/' . $from; + if ($locator->isStream($fromPath)) { + $fromPath = $locator->findResource($fromPath, true, true); + } + + if (!is_file($fromPath)) { + return; + } + + $mediaPath = dirname($fromPath); + $toPath = $mediaPath . '/' . $to; + if ($locator->isStream($toPath)) { + $toPath = $locator->findResource($toPath, true, true); + } + + if (is_file($toPath)) { + // TODO: translate error message + throw new RuntimeException(sprintf('File could not be renamed: %s already exists (%s)', $to, $mediaPath), 500); + } + + $result = rename($fromPath, $toPath); + if (!$result) { + // TODO: translate error message + throw new RuntimeException(sprintf('File could not be renamed: %s -> %s (%s)', $from, $to, $mediaPath), 500); + } + + // TODO: Add missing logic to handle retina files. + if (is_file($fromPath . '.meta.yaml')) { + $result = rename($fromPath . '.meta.yaml', $toPath . '.meta.yaml'); + if (!$result) { + // TODO: translate error message + throw new RuntimeException(sprintf('Meta could not be renamed: %s -> %s (%s)', $from, $to, $mediaPath), 500); + } + } + } + + /** + * Internal logic to remove file. + * + * @param string $filename + * @param string $path + */ + protected function doRemove(string $filename, string $path): void + { + $filesystem = Filesystem::getInstance(false); + + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + + // If path doesn't exist, there's nothing to do. + $pathname = $filesystem->pathname($filename); + if (!$this->fileExists($pathname, $path)) { + return; + } + + $folder = $locator->isStream($path) ? (string)$locator->findResource($path, true, true) : $path; + + // Remove requested media file. + if ($this->fileExists($filename, $path)) { + $result = unlink("{$folder}/{$filename}"); + if (!$result) { + throw new RuntimeException($this->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500); + } + } + + // Remove associated metadata. + $this->doRemoveMetadata($filename, $path); + + // Remove associated 2x, 3x and their .meta.yaml files. + $targetPath = rtrim(sprintf('%s/%s', $folder, $pathname), '/'); + $dir = scandir($targetPath, SCANDIR_SORT_NONE); + if (false === $dir) { + throw new RuntimeException($this->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500); + } + + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + + $basename = $filesystem->basename($filename); + $fileParts = (array)$filesystem->pathinfo($filename); + + foreach ($dir as $file) { + $preg_name = preg_quote($fileParts['filename'], '`'); + $preg_ext = preg_quote($fileParts['extension'] ?? '.', '`'); + $preg_filename = preg_quote($basename, '`'); + + if (preg_match("`({$preg_name}@\d+x\.{$preg_ext}(?:\.meta\.yaml)?$|{$preg_filename}\.meta\.yaml)$`", $file)) { + $testPath = $targetPath . '/' . $file; + if ($locator->isStream($testPath)) { + $testPath = (string)$locator->findResource($testPath, true, true); + $locator->clearCache($testPath); + } + + if (is_file($testPath)) { + $result = unlink($testPath); + if (!$result) { + throw new RuntimeException($this->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500); + } + } + } + } + } + + /** + * @param array $metadata + * @param string $filename + * @param string $path + */ + protected function doSaveMetadata(array $metadata, string $filename, string $path): void + { + $filepath = sprintf('%s/%s', $path, $filename); + + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + + // Do not use streams internally. + if ($locator->isStream($filepath)) { + $filepath = (string)$locator->findResource($filepath, true, true); + } + + $file = YamlFile::instance($filepath . '.meta.yaml'); + $file->save($metadata); + } + + /** + * @param string $filename + * @param string $path + */ + protected function doRemoveMetadata(string $filename, string $path): void + { + $filepath = sprintf('%s/%s', $path, $filename); + + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + + // Do not use streams internally. + if ($locator->isStream($filepath)) { + $filepath = (string)$locator->findResource($filepath, true); + if (!$filepath) { + return; + } + } + + $file = YamlFile::instance($filepath . '.meta.yaml'); + if ($file->exists()) { + $file->delete(); + } + } + + /** + * @param string $filename + * @param string $path + */ + protected function doSanitizeSvg(string $filename, string $path): void + { + $filepath = sprintf('%s/%s', $path, $filename); + + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + + // Do not use streams internally. + if ($locator->isStream($filepath)) { + $filepath = (string)$locator->findResource($filepath, true, true); + } + + Security::sanitizeSVG($filepath); + } + + /** + * @param string $name + * @param string $filename + * @param string $path + */ + protected function doAddUploadedMedium(string $name, string $filename, string $path): void + { + $filepath = sprintf('%s/%s', $path, $filename); + $medium = $this->createFromFile($filepath); + $realpath = $path . '/' . $name; + $this->add($realpath, $medium); + } + + /** + * @param string $string + * @return string + */ + protected function translate(string $string): string + { + return $this->getLanguage()->translate($string); + } + + abstract protected function getPath(): ?string; + + abstract protected function getGrav(): Grav; + + abstract protected function getConfig(): Config; + + abstract protected function getLanguage(): Language; + + abstract protected function clearCache(): void; +} diff --git a/system/src/Grav/Common/Media/Traits/StaticResizeTrait.php b/system/src/Grav/Common/Media/Traits/StaticResizeTrait.php new file mode 100644 index 0000000..49d75be --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/StaticResizeTrait.php @@ -0,0 +1,40 @@ +styleAttributes['width'] = $width . 'px'; + } else { + unset($this->styleAttributes['width']); + } + if ($height) { + $this->styleAttributes['height'] = $height . 'px'; + } else { + unset($this->styleAttributes['height']); + } + + return $this; + } +} diff --git a/system/src/Grav/Common/Media/Traits/ThumbnailMediaTrait.php b/system/src/Grav/Common/Media/Traits/ThumbnailMediaTrait.php new file mode 100644 index 0000000..eab7320 --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/ThumbnailMediaTrait.php @@ -0,0 +1,149 @@ +bubble('parsedownElement', [$title, $alt, $class, $id, $reset]); + } + + /** + * Return HTML markup from the medium. + * + * @param string|null $title + * @param string|null $alt + * @param string|null $class + * @param string|null $id + * @param bool $reset + * @return string + */ + public function html($title = null, $alt = null, $class = null, $id = null, $reset = true) + { + return $this->bubble('html', [$title, $alt, $class, $id, $reset]); + } + + /** + * Switch display mode. + * + * @param string $mode + * + * @return MediaLinkInterface|MediaObjectInterface|null + */ + public function display($mode = 'source') + { + return $this->bubble('display', [$mode], false); + } + + /** + * Switch thumbnail. + * + * @param string $type + * + * @return MediaLinkInterface|MediaObjectInterface + */ + public function thumbnail($type = 'auto') + { + $this->bubble('thumbnail', [$type], false); + + return $this->bubble('getThumbnail', [], false); + } + + /** + * Turn the current Medium into a Link + * + * @param bool $reset + * @param array $attributes + * @return MediaLinkInterface + */ + public function link($reset = true, array $attributes = []) + { + return $this->bubble('link', [$reset, $attributes], false); + } + + /** + * Turn the current Medium into a Link with lightbox enabled + * + * @param int|null $width + * @param int|null $height + * @param bool $reset + * @return MediaLinkInterface + */ + public function lightbox($width = null, $height = null, $reset = true) + { + return $this->bubble('lightbox', [$width, $height, $reset], false); + } + + /** + * Bubble a function call up to either the superclass function or the parent Medium instance + * + * @param string $method + * @param array $arguments + * @param bool $testLinked + * @return mixed + */ + protected function bubble($method, array $arguments = [], $testLinked = true) + { + if (!$testLinked || $this->linked) { + $parent = $this->parent; + if (null === $parent) { + return $this; + } + + $closure = [$parent, $method]; + + if (!is_callable($closure)) { + throw new BadMethodCallException(get_class($parent) . '::' . $method . '() not found.'); + } + + return $closure(...$arguments); + } + + return parent::{$method}(...$arguments); + } +} diff --git a/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php b/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php new file mode 100644 index 0000000..e03fbbd --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php @@ -0,0 +1,68 @@ +attributes['poster'] = $urlImage; + + return $this; + } + + /** + * Allows to set the playsinline attribute + * + * @param bool $status + * @return $this + */ + public function playsinline($status = false) + { + if ($status) { + $this->attributes['playsinline'] = true; + } else { + unset($this->attributes['playsinline']); + } + + return $this; + } + + /** + * Parsedown element for source display mode + * + * @param array $attributes + * @param bool $reset + * @return array + */ + protected function sourceParsedownElement(array $attributes, $reset = true) + { + $location = $this->url($reset); + + return [ + 'name' => 'video', + 'rawHtml' => 'Your browser does not support the video tag.', + 'attributes' => $attributes + ]; + } +} diff --git a/system/src/Grav/Common/Page/Collection.php b/system/src/Grav/Common/Page/Collection.php new file mode 100644 index 0000000..930aeda --- /dev/null +++ b/system/src/Grav/Common/Page/Collection.php @@ -0,0 +1,707 @@ +params = $params; + $this->pages = $pages ?: Grav::instance()->offsetGet('pages'); + } + + /** + * Get the collection params + * + * @return array + */ + public function params() + { + return $this->params; + } + + /** + * Set parameters to the Collection + * + * @param array $params + * @return $this + */ + public function setParams(array $params) + { + $this->params = array_merge($this->params, $params); + + return $this; + } + + /** + * Add a single page to a collection + * + * @param PageInterface $page + * @return $this + */ + public function addPage(PageInterface $page) + { + $this->items[$page->path()] = ['slug' => $page->slug()]; + + return $this; + } + + /** + * Add a page with path and slug + * + * @param string $path + * @param string $slug + * @return $this + */ + public function add($path, $slug) + { + $this->items[$path] = ['slug' => $slug]; + + return $this; + } + + /** + * + * Create a copy of this collection + * + * @return static + */ + public function copy() + { + return new static($this->items, $this->params, $this->pages); + } + + /** + * + * Merge another collection with the current collection + * + * @param PageCollectionInterface $collection + * @return $this + */ + public function merge(PageCollectionInterface $collection) + { + foreach ($collection as $page) { + $this->addPage($page); + } + + return $this; + } + + /** + * Intersect another collection with the current collection + * + * @param PageCollectionInterface $collection + * @return $this + */ + public function intersect(PageCollectionInterface $collection) + { + $array1 = $this->items; + $array2 = $collection->toArray(); + + $this->items = array_uintersect($array1, $array2, function ($val1, $val2) { + return strcmp($val1['slug'], $val2['slug']); + }); + + return $this; + } + + /** + * Set current page. + */ + public function setCurrent(string $path): void + { + reset($this->items); + + while (($key = key($this->items)) !== null && $key !== $path) { + next($this->items); + } + } + + /** + * Returns current page. + * + * @return PageInterface + */ + public function current() + { + $current = parent::key(); + + return $this->pages->get($current); + } + + /** + * Returns current slug. + * + * @return mixed + */ + public function key() + { + $current = parent::current(); + + return $current['slug']; + } + + /** + * Returns the value at specified offset. + * + * @param string $offset + * @return PageInterface|null + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->pages->get($offset) ?: null; + } + + /** + * Split collection into array of smaller collections. + * + * @param int $size + * @return Collection[] + */ + public function batch($size) + { + $chunks = array_chunk($this->items, $size, true); + + $list = []; + foreach ($chunks as $chunk) { + $list[] = new static($chunk, $this->params, $this->pages); + } + + return $list; + } + + /** + * Remove item from the list. + * + * @param PageInterface|string|null $key + * @return $this + * @throws InvalidArgumentException + */ + public function remove($key = null) + { + if ($key instanceof PageInterface) { + $key = $key->path(); + } elseif (null === $key) { + $key = (string)key($this->items); + } + if (!is_string($key)) { + throw new InvalidArgumentException('Invalid argument $key.'); + } + + parent::remove($key); + + return $this; + } + + /** + * Reorder collection. + * + * @param string $by + * @param string $dir + * @param array|null $manual + * @param string|null $sort_flags + * @return $this + */ + public function order($by, $dir = 'asc', $manual = null, $sort_flags = null) + { + $this->items = $this->pages->sortCollection($this, $by, $dir, $manual, $sort_flags); + + return $this; + } + + /** + * Check to see if this item is the first in the collection. + * + * @param string $path + * @return bool True if item is first. + */ + public function isFirst($path): bool + { + return $this->items && $path === array_keys($this->items)[0]; + } + + /** + * Check to see if this item is the last in the collection. + * + * @param string $path + * @return bool True if item is last. + */ + public function isLast($path): bool + { + return $this->items && $path === array_keys($this->items)[count($this->items) - 1]; + } + + /** + * Gets the previous sibling based on current position. + * + * @param string $path + * + * @return PageInterface The previous item. + */ + public function prevSibling($path) + { + return $this->adjacentSibling($path, -1); + } + + /** + * Gets the next sibling based on current position. + * + * @param string $path + * + * @return PageInterface The next item. + */ + public function nextSibling($path) + { + return $this->adjacentSibling($path, 1); + } + + /** + * Returns the adjacent sibling based on a direction. + * + * @param string $path + * @param int $direction either -1 or +1 + * @return PageInterface|Collection The sibling item. + */ + public function adjacentSibling($path, $direction = 1) + { + $values = array_keys($this->items); + $keys = array_flip($values); + + if (array_key_exists($path, $keys)) { + $index = $keys[$path] - $direction; + + return isset($values[$index]) ? $this->offsetGet($values[$index]) : $this; + } + + return $this; + } + + /** + * Returns the item in the current position. + * + * @param string $path the path the item + * @return int|null The index of the current page, null if not found. + */ + public function currentPosition($path): ?int + { + $pos = array_search($path, array_keys($this->items), true); + + return $pos !== false ? $pos : null; + } + + /** + * Returns the items between a set of date ranges of either the page date field (default) or + * an arbitrary datetime page field where start date and end date are optional + * Dates must be passed in as text that strtotime() can process + * http://php.net/manual/en/function.strtotime.php + * + * @param string|null $startDate + * @param string|null $endDate + * @param string|null $field + * @return $this + * @throws Exception + */ + public function dateRange($startDate = null, $endDate = null, $field = null) + { + $start = $startDate ? Utils::date2timestamp($startDate) : null; + $end = $endDate ? Utils::date2timestamp($endDate) : null; + + $date_range = []; + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + if (!$page) { + continue; + } + + $date = $field ? strtotime($page->value($field)) : $page->date(); + + if ((!$start || $date >= $start) && (!$end || $date <= $end)) { + $date_range[$path] = $slug; + } + } + + $this->items = $date_range; + + return $this; + } + + /** + * Creates new collection with only visible pages + * + * @return Collection The collection with only visible pages + */ + public function visible() + { + $visible = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + if ($page !== null && $page->visible()) { + $visible[$path] = $slug; + } + } + $this->items = $visible; + + return $this; + } + + /** + * Creates new collection with only non-visible pages + * + * @return Collection The collection with only non-visible pages + */ + public function nonVisible() + { + $visible = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + if ($page !== null && !$page->visible()) { + $visible[$path] = $slug; + } + } + $this->items = $visible; + + return $this; + } + + /** + * Creates new collection with only pages + * + * @return Collection The collection with only pages + */ + public function pages() + { + $modular = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + if ($page !== null && !$page->isModule()) { + $modular[$path] = $slug; + } + } + $this->items = $modular; + + return $this; + } + + /** + * Creates new collection with only modules + * + * @return Collection The collection with only modules + */ + public function modules() + { + $modular = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + if ($page !== null && $page->isModule()) { + $modular[$path] = $slug; + } + } + $this->items = $modular; + + return $this; + } + + /** + * Alias of pages() + * + * @return Collection The collection with only non-module pages + */ + public function nonModular() + { + $this->pages(); + + return $this; + } + + /** + * Alias of modules() + * + * @return Collection The collection with only modules + */ + public function modular() + { + $this->modules(); + + return $this; + } + + /** + * Creates new collection with only translated pages + * + * @return Collection The collection with only published pages + * @internal + */ + public function translated() + { + $published = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + if ($page !== null && $page->translated()) { + $published[$path] = $slug; + } + } + $this->items = $published; + + return $this; + } + + /** + * Creates new collection with only untranslated pages + * + * @return Collection The collection with only non-published pages + * @internal + */ + public function nonTranslated() + { + $published = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + if ($page !== null && !$page->translated()) { + $published[$path] = $slug; + } + } + $this->items = $published; + + return $this; + } + + /** + * Creates new collection with only published pages + * + * @return Collection The collection with only published pages + */ + public function published() + { + $published = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + if ($page !== null && $page->published()) { + $published[$path] = $slug; + } + } + $this->items = $published; + + return $this; + } + + /** + * Creates new collection with only non-published pages + * + * @return Collection The collection with only non-published pages + */ + public function nonPublished() + { + $published = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + if ($page !== null && !$page->published()) { + $published[$path] = $slug; + } + } + $this->items = $published; + + return $this; + } + + /** + * Creates new collection with only routable pages + * + * @return Collection The collection with only routable pages + */ + public function routable() + { + $routable = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + + if ($page !== null && $page->routable()) { + $routable[$path] = $slug; + } + } + + $this->items = $routable; + + return $this; + } + + /** + * Creates new collection with only non-routable pages + * + * @return Collection The collection with only non-routable pages + */ + public function nonRoutable() + { + $routable = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + if ($page !== null && !$page->routable()) { + $routable[$path] = $slug; + } + } + $this->items = $routable; + + return $this; + } + + /** + * Creates new collection with only pages of the specified type + * + * @param string $type + * @return Collection The collection + */ + public function ofType($type) + { + $items = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + if ($page !== null && $page->template() === $type) { + $items[$path] = $slug; + } + } + + $this->items = $items; + + return $this; + } + + /** + * Creates new collection with only pages of one of the specified types + * + * @param string[] $types + * @return Collection The collection + */ + public function ofOneOfTheseTypes($types) + { + $items = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + if ($page !== null && in_array($page->template(), $types, true)) { + $items[$path] = $slug; + } + } + + $this->items = $items; + + return $this; + } + + /** + * Creates new collection with only pages of one of the specified access levels + * + * @param array $accessLevels + * @return Collection The collection + */ + public function ofOneOfTheseAccessLevels($accessLevels) + { + $items = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + + if ($page !== null && isset($page->header()->access)) { + if (is_array($page->header()->access)) { + //Multiple values for access + $valid = false; + + foreach ($page->header()->access as $index => $accessLevel) { + if (is_array($accessLevel)) { + foreach ($accessLevel as $innerIndex => $innerAccessLevel) { + if (in_array($innerAccessLevel, $accessLevels, false)) { + $valid = true; + } + } + } else { + if (in_array($index, $accessLevels, false)) { + $valid = true; + } + } + } + if ($valid) { + $items[$path] = $slug; + } + } else { + //Single value for access + if (in_array($page->header()->access, $accessLevels, false)) { + $items[$path] = $slug; + } + } + } + } + + $this->items = $items; + + return $this; + } + + /** + * Get the extended version of this Collection with each page keyed by route + * + * @return array + * @throws Exception + */ + public function toExtendedArray() + { + $items = []; + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + + if ($page !== null) { + $items[$page->route()] = $page->toArray(); + } + } + return $items; + } +} diff --git a/system/src/Grav/Common/Page/Header.php b/system/src/Grav/Common/Page/Header.php new file mode 100644 index 0000000..71c7b16 --- /dev/null +++ b/system/src/Grav/Common/Page/Header.php @@ -0,0 +1,37 @@ +toArray(); + } +} diff --git a/system/src/Grav/Common/Page/Interfaces/PageCollectionInterface.php b/system/src/Grav/Common/Page/Interfaces/PageCollectionInterface.php new file mode 100644 index 0000000..5002911 --- /dev/null +++ b/system/src/Grav/Common/Page/Interfaces/PageCollectionInterface.php @@ -0,0 +1,283 @@ +modules() instead + */ + public function modular(); + + /** + * Creates new collection with only non-module pages + * + * @return PageCollectionInterface The collection with only non-module pages + * @deprecated 1.7 Use $this->pages() instead + */ + public function nonModular(); + + /** + * Creates new collection with only published pages + * + * @return PageCollectionInterface The collection with only published pages + */ + public function published(); + + /** + * Creates new collection with only non-published pages + * + * @return PageCollectionInterface The collection with only non-published pages + */ + public function nonPublished(); + + /** + * Creates new collection with only routable pages + * + * @return PageCollectionInterface The collection with only routable pages + */ + public function routable(); + + /** + * Creates new collection with only non-routable pages + * + * @return PageCollectionInterface The collection with only non-routable pages + */ + public function nonRoutable(); + + /** + * Creates new collection with only pages of the specified type + * + * @param string $type + * @return PageCollectionInterface The collection + */ + public function ofType($type); + + /** + * Creates new collection with only pages of one of the specified types + * + * @param string[] $types + * @return PageCollectionInterface The collection + */ + public function ofOneOfTheseTypes($types); + + /** + * Creates new collection with only pages of one of the specified access levels + * + * @param array $accessLevels + * @return PageCollectionInterface The collection + */ + public function ofOneOfTheseAccessLevels($accessLevels); + + /** + * Converts collection into an array. + * + * @return array + */ + public function toArray(); + + /** + * Get the extended version of this Collection with each page keyed by route + * + * @return array + * @throws Exception + */ + public function toExtendedArray(); +} diff --git a/system/src/Grav/Common/Page/Interfaces/PageContentInterface.php b/system/src/Grav/Common/Page/Interfaces/PageContentInterface.php new file mode 100644 index 0000000..d9d3105 --- /dev/null +++ b/system/src/Grav/Common/Page/Interfaces/PageContentInterface.php @@ -0,0 +1,257 @@ +true) for example + * + * @param array|null $var New array of name value pairs where the name is the process and value is true or false + * @return array Array of name value pairs where the name is the process and value is true or false + */ + public function process($var = null); + + /** + * Gets and Sets the slug for the Page. The slug is used in the URL routing. If not set it uses + * the parent folder from the path + * + * @param string|null $var New slug, e.g. 'my-blog' + * @return string The slug + */ + public function slug($var = null); + + /** + * Get/set order number of this page. + * + * @param int|null $var New order as a number + * @return string|bool Order in a form of '02.' or false if not set + */ + public function order($var = null); + + /** + * Gets and sets the identifier for this Page object. + * + * @param string|null $var New identifier + * @return string The identifier + */ + public function id($var = null); + + /** + * Gets and sets the modified timestamp. + * + * @param int|null $var New modified unix timestamp + * @return int Modified unix timestamp + */ + public function modified($var = null); + + /** + * Gets and sets the option to show the last_modified header for the page. + * + * @param bool|null $var New last_modified header value + * @return bool Show last_modified header + */ + public function lastModified($var = null); + + /** + * Get/set the folder. + * + * @param string|null $var New folder + * @return string|null The folder + */ + public function folder($var = null); + + /** + * Gets and sets the date for this Page object. This is typically passed in via the page headers + * + * @param string|null $var New string representation of a date + * @return int Unix timestamp representation of the date + */ + public function date($var = null); + + /** + * Gets and sets the date format for this Page object. This is typically passed in via the page headers + * using typical PHP date string structure - http://php.net/manual/en/function.date.php + * + * @param string|null $var New string representation of a date format + * @return string String representation of a date format + */ + public function dateformat($var = null); + + /** + * Gets and sets the taxonomy array which defines which taxonomies this page identifies itself with. + * + * @param array|null $var New array of taxonomies + * @return array An array of taxonomies + */ + public function taxonomy($var = null); + + /** + * Gets the configured state of the processing method. + * + * @param string $process The process name, eg "twig" or "markdown" + * @return bool Whether or not the processing method is enabled for this Page + */ + public function shouldProcess($process); + + /** + * Returns true if page is a module. + * + * @return bool + */ + public function isModule(): bool; + + /** + * Returns whether or not this Page object has a .md file associated with it or if its just a directory. + * + * @return bool True if its a page with a .md file associated + */ + public function isPage(); + + /** + * Returns whether or not this Page object is a directory or a page. + * + * @return bool True if its a directory + */ + public function isDir(); + + /** + * Returns whether the page exists in the filesystem. + * + * @return bool + */ + public function exists(); +} diff --git a/system/src/Grav/Common/Page/Interfaces/PageFormInterface.php b/system/src/Grav/Common/Page/Interfaces/PageFormInterface.php new file mode 100644 index 0000000..3c88ebf --- /dev/null +++ b/system/src/Grav/Common/Page/Interfaces/PageFormInterface.php @@ -0,0 +1,33 @@ + blueprint, ...], where blueprint follows the regular form blueprint format. + * + * @return array + */ + //public function getForms(): array; + + /** + * Add forms to this page. + * + * @param array $new + * @return $this + */ + public function addForms(array $new/*, $override = true*/); + + /** + * Alias of $this->getForms(); + * + * @return array + */ + public function forms();//: array; +} diff --git a/system/src/Grav/Common/Page/Interfaces/PageInterface.php b/system/src/Grav/Common/Page/Interfaces/PageInterface.php new file mode 100644 index 0000000..e5ea20c --- /dev/null +++ b/system/src/Grav/Common/Page/Interfaces/PageInterface.php @@ -0,0 +1,25 @@ +save() in order to perform the move. + * + * @param PageInterface $parent New parent page. + * @return $this + */ + public function move(PageInterface $parent); + + /** + * Prepare a copy from the page. Copies also everything that's under the current page. + * + * Returns a new Page object for the copy. + * You need to call $this->save() in order to perform the move. + * + * @param PageInterface $parent New parent page. + * @return $this + */ + public function copy(PageInterface $parent); + + /** + * Get blueprints for the page. + * + * @return Blueprint + */ + public function blueprints(); + + /** + * Get the blueprint name for this page. Use the blueprint form field if set + * + * @return string + */ + public function blueprintName(); + + /** + * Validate page header. + * + * @throws Exception + */ + public function validate(); + + /** + * Filter page header from illegal contents. + */ + public function filter(); + + /** + * Get unknown header variables. + * + * @return array + */ + public function extra(); + + /** + * Convert page to an array. + * + * @return array + */ + public function toArray(); + + /** + * Convert page to YAML encoded string. + * + * @return string + */ + public function toYaml(); + + /** + * Convert page to JSON encoded string. + * + * @return string + */ + public function toJson(); + + /** + * Returns normalized list of name => form pairs. + * + * @return array + */ + public function forms(); + + /** + * @param array $new + */ + public function addForms(array $new); + + /** + * Gets and sets the name field. If no name field is set, it will return 'default.md'. + * + * @param string|null $var The name of this page. + * @return string The name of this page. + */ + public function name($var = null); + + /** + * Returns child page type. + * + * @return string + */ + public function childType(); + + /** + * Gets and sets the template field. This is used to find the correct Twig template file to render. + * If no field is set, it will return the name without the .md extension + * + * @param string|null $var the template name + * @return string the template name + */ + public function template($var = null); + + /** + * Allows a page to override the output render format, usually the extension provided + * in the URL. (e.g. `html`, `json`, `xml`, etc). + * + * @param string|null $var + * @return string + */ + public function templateFormat($var = null); + + /** + * Gets and sets the extension field. + * + * @param string|null $var + * @return string|null + */ + public function extension($var = null); + + /** + * Gets and sets the expires field. If not set will return the default + * + * @param int|null $var The new expires value. + * @return int The expires value + */ + public function expires($var = null); + + /** + * Gets and sets the cache-control property. If not set it will return the default value (null) + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details on valid options + * + * @param string|null $var + * @return string|null + */ + public function cacheControl($var = null); + + /** + * @param bool|null $var + * @return bool + */ + public function ssl($var = null); + + /** + * Returns the state of the debugger override etting for this page + * + * @return bool + */ + public function debugger(); + + /** + * Function to merge page metadata tags and build an array of Metadata objects + * that can then be rendered in the page. + * + * @param array|null $var an Array of metadata values to set + * @return array an Array of metadata values for the page + */ + public function metadata($var = null); + + /** + * Gets and sets the option to show the etag header for the page. + * + * @param bool|null $var show etag header + * @return bool show etag header + */ + public function eTag($var = null): bool; + + /** + * Gets and sets the path to the .md file for this Page object. + * + * @param string|null $var the file path + * @return string|null the file path + */ + public function filePath($var = null); + + /** + * Gets the relative path to the .md file + * + * @return string The relative file path + */ + public function filePathClean(); + + /** + * Gets and sets the order by which any sub-pages should be sorted. + * + * @param string|null $var the order, either "asc" or "desc" + * @return string the order, either "asc" or "desc" + * @deprecated 1.6 + */ + public function orderDir($var = null); + + /** + * Gets and sets the order by which the sub-pages should be sorted. + * + * default - is the order based on the file system, ie 01.Home before 02.Advark + * title - is the order based on the title set in the pages + * date - is the order based on the date set in the pages + * folder - is the order based on the name of the folder with any numerics omitted + * + * @param string|null $var supported options include "default", "title", "date", and "folder" + * @return string supported options include "default", "title", "date", and "folder" + * @deprecated 1.6 + */ + public function orderBy($var = null); + + /** + * Gets the manual order set in the header. + * + * @param string|null $var supported options include "default", "title", "date", and "folder" + * @return array + * @deprecated 1.6 + */ + public function orderManual($var = null); + + /** + * Gets and sets the maxCount field which describes how many sub-pages should be displayed if the + * sub_pages header property is set for this page object. + * + * @param int|null $var the maximum number of sub-pages + * @return int the maximum number of sub-pages + * @deprecated 1.6 + */ + public function maxCount($var = null); + + /** + * Gets and sets the modular var that helps identify this page is a modular child + * + * @param bool|null $var true if modular_twig + * @return bool true if modular_twig + * @deprecated 1.7 Use ->isModule() or ->modularTwig() method instead. + */ + public function modular($var = null); + + /** + * Gets and sets the modular_twig var that helps identify this page as a modular child page that will need + * twig processing handled differently from a regular page. + * + * @param bool|null $var true if modular_twig + * @return bool true if modular_twig + */ + public function modularTwig($var = null); + + /** + * Returns children of this page. + * + * @return PageCollectionInterface|Collection + */ + public function children(); + + /** + * Check to see if this item is the first in an array of sub-pages. + * + * @return bool True if item is first. + */ + public function isFirst(); + + /** + * Check to see if this item is the last in an array of sub-pages. + * + * @return bool True if item is last + */ + public function isLast(); + + /** + * Gets the previous sibling based on current position. + * + * @return PageInterface the previous Page item + */ + public function prevSibling(); + + /** + * Gets the next sibling based on current position. + * + * @return PageInterface the next Page item + */ + public function nextSibling(); + + /** + * Returns the adjacent sibling based on a direction. + * + * @param int $direction either -1 or +1 + * @return PageInterface|false the sibling page + */ + public function adjacentSibling($direction = 1); + + /** + * Helper method to return an ancestor page. + * + * @param bool|null $lookup Name of the parent folder + * @return PageInterface page you were looking for if it exists + */ + public function ancestor($lookup = null); + + /** + * Helper method to return an ancestor page to inherit from. The current + * page object is returned. + * + * @param string $field Name of the parent folder + * @return PageInterface + */ + public function inherited($field); + + /** + * Helper method to return an ancestor field only to inherit from. The + * first occurrence of an ancestor field will be returned if at all. + * + * @param string $field Name of the parent folder + * @return array + */ + public function inheritedField($field); + + /** + * Helper method to return a page. + * + * @param string $url the url of the page + * @param bool $all + * @return PageInterface page you were looking for if it exists + */ + public function find($url, $all = false); + + /** + * Get a collection of pages in the current context. + * + * @param string|array $params + * @param bool $pagination + * @return Collection + * @throws InvalidArgumentException + */ + public function collection($params = 'content', $pagination = true); + + /** + * @param string|array $value + * @param bool $only_published + * @return PageCollectionInterface|Collection + */ + public function evaluate($value, $only_published = true); + + /** + * Returns whether or not the current folder exists + * + * @return bool + */ + public function folderExists(); + + /** + * Gets the Page Unmodified (original) version of the page. + * + * @return PageInterface The original version of the page. + */ + public function getOriginal(); + + /** + * Gets the action. + * + * @return string The Action string. + */ + public function getAction(); +} diff --git a/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php b/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php new file mode 100644 index 0000000..2900266 --- /dev/null +++ b/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php @@ -0,0 +1,180 @@ +page = $page ?? Grav::instance()['page'] ?? null; + + // Add defaults to the configuration. + if (null === $config || !isset($config['markdown'], $config['images'])) { + $c = Grav::instance()['config']; + $config = $config ?? []; + $config += [ + 'markdown' => $c->get('system.pages.markdown', []), + 'images' => $c->get('system.images', []) + ]; + } + + $this->config = $config; + } + + /** + * @return PageInterface|null + */ + public function getPage(): ?PageInterface + { + return $this->page; + } + + /** + * @return array + */ + public function getConfig(): array + { + return $this->config; + } + + /** + * @param object $markdown + * @return void + */ + public function fireInitializedEvent($markdown): void + { + $grav = Grav::instance(); + + $grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $markdown, 'page' => $this->page])); + } + + /** + * Process a Link excerpt + * + * @param array $excerpt + * @param string $type + * @return array + */ + public function processLinkExcerpt(array $excerpt, string $type = 'link'): array + { + $grav = Grav::instance(); + $url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href'])); + $url_parts = $this->parseUrl($url); + + // If there is a query, then parse it and build action calls. + if (isset($url_parts['query'])) { + $actions = array_reduce( + explode('&', $url_parts['query']), + static function ($carry, $item) { + $parts = explode('=', $item, 2); + $value = isset($parts[1]) ? rawurldecode($parts[1]) : true; + $carry[$parts[0]] = $value; + + return $carry; + }, + [] + ); + + // Valid attributes supported. + $valid_attributes = $grav['config']->get('system.pages.markdown.valid_link_attributes') ?? []; + + $skip = []; + // Unless told to not process, go through actions. + if (array_key_exists('noprocess', $actions)) { + $skip = is_bool($actions['noprocess']) ? $actions : explode(',', $actions['noprocess']); + unset($actions['noprocess']); + } + + // Loop through actions for the image and call them. + foreach ($actions as $attrib => $value) { + if (!in_array($attrib, $skip)) { + $key = $attrib; + + if (in_array($attrib, $valid_attributes, true)) { + // support both class and classes. + if ($attrib === 'classes') { + $attrib = 'class'; + } + $excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value); + unset($actions[$key]); + } + } + } + + $url_parts['query'] = http_build_query($actions, '', '&', PHP_QUERY_RFC3986); + } + + // If no query elements left, unset query. + if (empty($url_parts['query'])) { + unset($url_parts['query']); + } + + // Set path to / if not set. + if (empty($url_parts['path'])) { + $url_parts['path'] = ''; + } + + // If scheme isn't http(s).. + if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) { + // Handle custom streams. + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + if ($type === 'link' && $locator->isStream($url)) { + $path = $locator->findResource($url, false) ?: $locator->findResource($url, false, true); + $url_parts['path'] = $grav['base_url_relative'] . '/' . $path; + unset($url_parts['stream'], $url_parts['scheme']); + } + + $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); + + return $excerpt; + } + + // Handle paths and such. + $url_parts = Uri::convertUrl($this->page, $url_parts, $type); + + // Build the URL from the component parts and set it on the element. + $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); + + return $excerpt; + } + + /** + * Process an image excerpt + * + * @param array $excerpt + * @return array + */ + public function processImageExcerpt(array $excerpt): array + { + $url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src'])); + $url_parts = $this->parseUrl($url); + + $media = null; + $filename = null; + + if (!empty($url_parts['stream'])) { + $filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? ''); + + $media = $this->page->getMedia(); + } else { + $grav = Grav::instance(); + /** @var Pages $pages */ + $pages = $grav['pages']; + + // File is also local if scheme is http(s) and host matches. + $local_file = isset($url_parts['path']) + && (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true)) + && (empty($url_parts['host']) || $url_parts['host'] === $grav['uri']->host()); + + if ($local_file) { + $filename = basename($url_parts['path']); + $folder = dirname($url_parts['path']); + + // Get the local path to page media if possible. + if ($this->page && $folder === $this->page->url(false, false, false)) { + // Get the media objects for this page. + $media = $this->page->getMedia(); + } else { + // see if this is an external page to this one + $base_url = rtrim($grav['base_url_relative'] . $pages->base(), '/'); + $page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/'); + + $ext_page = $pages->find($page_route, true); + if ($ext_page) { + $media = $ext_page->getMedia(); + } else { + $grav->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media])); + } + } + } + } + + // If there is a media file that matches the path referenced.. + if ($media && $filename && isset($media[$filename])) { + // Get the medium object. + /** @var Medium $medium */ + $medium = $media[$filename]; + + // Process operations + $medium = $this->processMediaActions($medium, $url_parts); + $element_excerpt = $excerpt['element']['attributes']; + + $alt = $element_excerpt['alt'] ?? ''; + $title = $element_excerpt['title'] ?? ''; + $class = $element_excerpt['class'] ?? ''; + $id = $element_excerpt['id'] ?? ''; + + $excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true); + } else { + // Not a current page media file, see if it needs converting to relative. + $excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts); + } + + return $excerpt; + } + + /** + * Process media actions + * + * @param Medium $medium + * @param string|array $url + * @return Medium|Link + */ + public function processMediaActions($medium, $url) + { + $url_parts = is_string($url) ? $this->parseUrl($url) : $url; + $actions = []; + + + // if there is a query, then parse it and build action calls + if (isset($url_parts['query'])) { + $actions = array_reduce( + explode('&', $url_parts['query']), + static function ($carry, $item) { + $parts = explode('=', $item, 2); + $value = $parts[1] ?? null; + $carry[] = ['method' => $parts[0], 'params' => $value]; + + return $carry; + }, + [] + ); + } + + $defaults = $this->config['images']['defaults'] ?? []; + if (count($defaults)) { + foreach ($defaults as $method => $params) { + if (array_search($method, array_column($actions, 'method')) === false) { + $actions[] = [ + 'method' => $method, + 'params' => $params, + ]; + } + } + } + + // loop through actions for the image and call them + foreach ($actions as $action) { + $matches = []; + + if (preg_match('/\[(.*)\]/', $action['params'], $matches)) { + $args = [explode(',', $matches[1])]; + } else { + $args = explode(',', $action['params']); + } + + $medium = call_user_func_array([$medium, $action['method']], $args); + } + + if (isset($url_parts['fragment'])) { + $medium->urlHash($url_parts['fragment']); + } + + return $medium; + } + + /** + * Variation of parse_url() which works also with local streams. + * + * @param string $url + * @return array + */ + protected function parseUrl(string $url) + { + $url_parts = Utils::multibyteParseUrl($url); + + if (isset($url_parts['scheme'])) { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + // Special handling for the streams. + if ($locator->schemeExists($url_parts['scheme'])) { + if (isset($url_parts['host'])) { + // Merge host and path into a path. + $url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : ''); + unset($url_parts['host']); + } + + $url_parts['stream'] = true; + } + } + + return $url_parts; + } +} diff --git a/system/src/Grav/Common/Page/Media.php b/system/src/Grav/Common/Page/Media.php new file mode 100644 index 0000000..e182014 --- /dev/null +++ b/system/src/Grav/Common/Page/Media.php @@ -0,0 +1,239 @@ +setPath($path); + $this->media_order = $media_order; + + $this->__wakeup(); + if ($load) { + $this->init(); + } + } + + /** + * Initialize static variables on unserialize. + */ + public function __wakeup() + { + if (null === static::$global) { + // Add fallback to global media. + static::$global = GlobalMedia::getInstance(); + } + } + + /** + * @param string $offset + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + return parent::offsetExists($offset) ?: isset(static::$global[$offset]); + } + + /** + * @param string $offset + * @return MediaObjectInterface|null + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return parent::offsetGet($offset) ?: static::$global[$offset]; + } + + /** + * Initialize class. + * + * @return void + */ + protected function init() + { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $config = Grav::instance()['config']; + $exif_reader = isset(Grav::instance()['exif']) ? Grav::instance()['exif']->getReader() : false; + $media_types = array_keys(Grav::instance()['config']->get('media.types')); + $path = $this->getPath(); + + // Handle special cases where page doesn't exist in filesystem. + if (!$path || !is_dir($path)) { + return; + } + + $iterator = new FilesystemIterator($path, FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS); + + $media = []; + + foreach ($iterator as $file => $info) { + // Ignore folders and Markdown files. + $filename = $info->getFilename(); + if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || strpos($filename, '.') === 0) { + continue; + } + + // Find out what type we're dealing with + [$basename, $ext, $type, $extra] = $this->getFileParts($filename); + + if (!in_array(strtolower($ext), $media_types, true)) { + continue; + } + + if ($type === 'alternative') { + $media["{$basename}.{$ext}"][$type][$extra] = ['file' => $file, 'size' => $info->getSize()]; + } else { + $media["{$basename}.{$ext}"][$type] = ['file' => $file, 'size' => $info->getSize()]; + } + } + + foreach ($media as $name => $types) { + // First prepare the alternatives in case there is no base medium + if (!empty($types['alternative'])) { + /** + * @var string|int $ratio + * @var array $alt + */ + foreach ($types['alternative'] as $ratio => &$alt) { + $alt['file'] = $this->createFromFile($alt['file']); + + if (empty($alt['file'])) { + unset($types['alternative'][$ratio]); + } else { + $alt['file']->set('size', $alt['size']); + } + } + unset($alt); + } + + $file_path = null; + + // Create the base medium + if (empty($types['base'])) { + if (!isset($types['alternative'])) { + continue; + } + + $max = max(array_keys($types['alternative'])); + $medium = $types['alternative'][$max]['file']; + $file_path = $medium->path(); + $medium = MediumFactory::scaledFromMedium($medium, $max, 1)['file']; + } else { + $medium = $this->createFromFile($types['base']['file']); + if ($medium) { + $medium->set('size', $types['base']['size']); + $file_path = $medium->path(); + } + } + + if (empty($medium)) { + continue; + } + + // metadata file + $meta_path = $file_path . '.meta.yaml'; + + if (file_exists($meta_path)) { + $types['meta']['file'] = $meta_path; + } elseif ($file_path && $exif_reader && $medium->get('mime') === 'image/jpeg' && empty($types['meta']) && $config->get('system.media.auto_metadata_exif')) { + $meta = $exif_reader->read($file_path); + + if ($meta) { + $meta_data = $meta->getData(); + $meta_trimmed = array_diff_key($meta_data, array_flip($this->standard_exif)); + if ($meta_trimmed) { + if ($locator->isStream($meta_path)) { + $file = File::instance($locator->findResource($meta_path, true, true)); + } else { + $file = File::instance($meta_path); + } + $file->save(Yaml::dump($meta_trimmed)); + $types['meta']['file'] = $meta_path; + } + } + } + + if (!empty($types['meta'])) { + $medium->addMetaFile($types['meta']['file']); + } + + if (!empty($types['thumb'])) { + // We will not turn it into medium yet because user might never request the thumbnail + // not wasting any resources on that, maybe we should do this for medium in general? + $medium->set('thumbnails.page', $types['thumb']['file']); + } + + // Build missing alternatives + if (!empty($types['alternative'])) { + $alternatives = $types['alternative']; + $max = max(array_keys($alternatives)); + + for ($i=$max; $i > 1; $i--) { + if (isset($alternatives[$i])) { + continue; + } + + $types['alternative'][$i] = MediumFactory::scaledFromMedium($alternatives[$max]['file'], $max, $i); + } + + foreach ($types['alternative'] as $altMedium) { + if ($altMedium['file'] != $medium) { + $altWidth = $altMedium['file']->get('width'); + $medWidth = $medium->get('width'); + if ($altWidth && $medWidth) { + $ratio = $altWidth / $medWidth; + $medium->addAlternative($ratio, $altMedium['file']); + } + } + } + } + + $this->add($name, $medium); + } + } + + /** + * @return string|null + * @deprecated 1.6 Use $this->getPath() instead. + */ + public function path(): ?string + { + return $this->getPath(); + } +} diff --git a/system/src/Grav/Common/Page/Medium/AbstractMedia.php b/system/src/Grav/Common/Page/Medium/AbstractMedia.php new file mode 100644 index 0000000..db81ddd --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/AbstractMedia.php @@ -0,0 +1,343 @@ +path; + } + + /** + * @param string|null $path + * @return void + */ + public function setPath(?string $path): void + { + $this->path = $path; + } + + /** + * Get medium by filename. + * + * @param string $filename + * @return MediaObjectInterface|null + */ + public function get($filename) + { + return $this->offsetGet($filename); + } + + /** + * Call object as function to get medium by filename. + * + * @param string $filename + * @return mixed + */ + public function __invoke($filename) + { + return $this->offsetGet($filename); + } + + /** + * Set file modification timestamps (query params) for all the media files. + * + * @param string|int|null $timestamp + * @return $this + */ + public function setTimestamps($timestamp = null) + { + foreach ($this->items as $instance) { + $instance->setTimestamp($timestamp); + } + + return $this; + } + + /** + * Get a list of all media. + * + * @return MediaObjectInterface[] + */ + public function all() + { + $this->items = $this->orderMedia($this->items); + + return $this->items; + } + + /** + * Get a list of all image media. + * + * @return MediaObjectInterface[] + */ + public function images() + { + $this->images = $this->orderMedia($this->images); + + return $this->images; + } + + /** + * Get a list of all video media. + * + * @return MediaObjectInterface[] + */ + public function videos() + { + $this->videos = $this->orderMedia($this->videos); + + return $this->videos; + } + + /** + * Get a list of all audio media. + * + * @return MediaObjectInterface[] + */ + public function audios() + { + $this->audios = $this->orderMedia($this->audios); + + return $this->audios; + } + + /** + * Get a list of all file media. + * + * @return MediaObjectInterface[] + */ + public function files() + { + $this->files = $this->orderMedia($this->files); + + return $this->files; + } + + /** + * @param string $name + * @param MediaObjectInterface|null $file + * @return void + */ + public function add($name, $file) + { + if (null === $file) { + return; + } + + $this->offsetSet($name, $file); + + switch ($file->type) { + case 'image': + $this->images[$name] = $file; + break; + case 'video': + $this->videos[$name] = $file; + break; + case 'audio': + $this->audios[$name] = $file; + break; + default: + $this->files[$name] = $file; + } + } + + /** + * @param string $name + * @return void + */ + public function hide($name) + { + $this->offsetUnset($name); + + unset($this->images[$name], $this->videos[$name], $this->audios[$name], $this->files[$name]); + } + + /** + * Create Medium from a file. + * + * @param string $file + * @param array $params + * @return Medium|null + */ + public function createFromFile($file, array $params = []) + { + return MediumFactory::fromFile($file, $params); + } + + /** + * Create Medium from array of parameters + * + * @param array $items + * @param Blueprint|null $blueprint + * @return Medium|null + */ + public function createFromArray(array $items = [], Blueprint $blueprint = null) + { + return MediumFactory::fromArray($items, $blueprint); + } + + /** + * @param MediaObjectInterface $mediaObject + * @return ImageFile + */ + public function getImageFileObject(MediaObjectInterface $mediaObject): ImageFile + { + return ImageFile::open($mediaObject->get('filepath')); + } + + /** + * Order the media based on the page's media_order + * + * @param array $media + * @return array + */ + protected function orderMedia($media) + { + if (null === $this->media_order) { + $path = $this->getPath(); + if (null !== $path) { + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + $page = $pages->get($path); + if ($page && isset($page->header()->media_order)) { + $this->media_order = array_map('trim', explode(',', $page->header()->media_order)); + } + } + } + + if (!empty($this->media_order) && is_array($this->media_order)) { + $media = Utils::sortArrayByArray($media, $this->media_order); + } else { + ksort($media, SORT_NATURAL | SORT_FLAG_CASE); + } + + return $media; + } + + protected function fileExists(string $filename, string $destination): bool + { + return file_exists("{$destination}/{$filename}"); + } + + /** + * Get filename, extension and meta part. + * + * @param string $filename + * @return array + */ + protected function getFileParts($filename) + { + if (preg_match('/(.*)@(\d+)x\.(.*)$/', $filename, $matches)) { + $name = $matches[1]; + $extension = $matches[3]; + $extra = (int) $matches[2]; + $type = 'alternative'; + + if ($extra === 1) { + $type = 'base'; + $extra = null; + } + } else { + $fileParts = explode('.', $filename); + + $name = array_shift($fileParts); + $extension = null; + $extra = null; + $type = 'base'; + + while (($part = array_shift($fileParts)) !== null) { + if ($part !== 'meta' && $part !== 'thumb') { + if (null !== $extension) { + $name .= '.' . $extension; + } + $extension = $part; + } else { + $type = $part; + $extra = '.' . $part . '.' . implode('.', $fileParts); + break; + } + } + } + + return [$name, $extension, $type, $extra]; + } + + protected function getGrav(): Grav + { + return Grav::instance(); + } + + protected function getConfig(): Config + { + return $this->getGrav()['config']; + } + + protected function getLanguage(): Language + { + return $this->getGrav()['language']; + } + + protected function clearCache(): void + { + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + $locator->clearCache(); + } +} diff --git a/system/src/Grav/Common/Page/Medium/AudioMedium.php b/system/src/Grav/Common/Page/Medium/AudioMedium.php new file mode 100644 index 0000000..f34f0a9 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/AudioMedium.php @@ -0,0 +1,36 @@ +resetPlayer(); + + return $this; + } +} diff --git a/system/src/Grav/Common/Page/Medium/GlobalMedia.php b/system/src/Grav/Common/Page/Medium/GlobalMedia.php new file mode 100644 index 0000000..50d69c7 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/GlobalMedia.php @@ -0,0 +1,149 @@ +resolveStream($offset)); + } + + /** + * @param string $offset + * @return MediaObjectInterface|null + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return parent::offsetGet($offset) ?: $this->addMedium($offset); + } + + /** + * @param string $filename + * @return string|null + */ + protected function resolveStream($filename) + { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + if (!$locator->isStream($filename)) { + return null; + } + + return $locator->findResource($filename) ?: null; + } + + /** + * @param string $stream + * @return MediaObjectInterface|null + */ + protected function addMedium($stream) + { + $filename = $this->resolveStream($stream); + if (!$filename) { + return null; + } + + $path = dirname($filename); + [$basename, $ext,, $extra] = $this->getFileParts(basename($filename)); + $medium = MediumFactory::fromFile($filename); + + if (null === $medium) { + return null; + } + + $medium->set('size', filesize($filename)); + $scale = (int) ($extra ?: 1); + + if ($scale !== 1) { + $altMedium = $medium; + + // Create scaled down regular sized image. + $medium = MediumFactory::scaledFromMedium($altMedium, $scale, 1)['file']; + + if (empty($medium)) { + return null; + } + + // Add original sized image as alternative. + $medium->addAlternative($scale, $altMedium['file']); + + // Locate or generate smaller retina images. + for ($i = $scale-1; $i > 1; $i--) { + $altFilename = "{$path}/{$basename}@{$i}x.{$ext}"; + + if (file_exists($altFilename)) { + $scaled = MediumFactory::fromFile($altFilename); + } else { + $scaled = MediumFactory::scaledFromMedium($altMedium, $scale, $i)['file']; + } + + if ($scaled) { + $medium->addAlternative($i, $scaled); + } + } + } + + $meta = "{$path}/{$basename}.{$ext}.yaml"; + if (file_exists($meta)) { + $medium->addMetaFile($meta); + } + $meta = "{$path}/{$basename}.{$ext}.meta.yaml"; + if (file_exists($meta)) { + $medium->addMetaFile($meta); + } + + $thumb = "{$path}/{$basename}.thumb.{$ext}"; + if (file_exists($thumb)) { + $medium->set('thumbnails.page', $thumb); + } + + $this->add($stream, $medium); + + return $medium; + } +} diff --git a/system/src/Grav/Common/Page/Medium/ImageFile.php b/system/src/Grav/Common/Page/Medium/ImageFile.php new file mode 100644 index 0000000..bd853bf --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/ImageFile.php @@ -0,0 +1,211 @@ +adapter; + if ($adapter) { + $adapter->deinit(); + } + } + + /** + * Clear previously applied operations + * + * @return void + */ + public function clearOperations() + { + $this->operations = []; + } + + /** + * This is the same as the Gregwar Image class except this one fires a Grav Event on creation of new cached file + * + * @param string $type the image type + * @param int $quality the quality (for JPEG) + * @param bool $actual + * @param array $extras + * @return string + */ + public function cacheFile($type = 'jpg', $quality = 80, $actual = false, $extras = []) + { + if ($type === 'guess') { + $type = $this->guessType(); + } + + if (!$this->forceCache && !count($this->operations) && $type === $this->guessType()) { + return $this->getFilename($this->getFilePath()); + } + + // Computes the hash + $this->hash = $this->getHash($type, $quality, $extras); + + /** @var Config $config */ + $config = Grav::instance()['config']; + + // Seo friendly image names + $seofriendly = $config->get('system.images.seofriendly', false); + + if ($seofriendly) { + $mini_hash = substr($this->hash, 0, 4) . substr($this->hash, -4); + $cacheFile = "{$this->prettyName}-{$mini_hash}"; + } else { + $cacheFile = "{$this->hash}-{$this->prettyName}"; + } + + $cacheFile .= '.' . $type; + + // If the files does not exists, save it + $image = $this; + + // Target file should be younger than all the current image + // dependencies + $conditions = array( + 'younger-than' => $this->getDependencies() + ); + + // The generating function + $generate = function ($target) use ($image, $type, $quality) { + $result = $image->save($target, $type, $quality); + + if ($result !== $target) { + throw new GenerationError($result); + } + + Grav::instance()->fireEvent('onImageMediumSaved', new Event(['image' => $target])); + }; + + // Asking the cache for the cacheFile + try { + $perms = $config->get('system.images.cache_perms', '0755'); + $perms = octdec($perms); + $file = $this->getCacheSystem()->setDirectoryMode($perms)->getOrCreateFile($cacheFile, $conditions, $generate, $actual); + } catch (GenerationError $e) { + $file = $e->getNewFile(); + } + + // Nulling the resource + $adapter = $this->getAdapter(); + $adapter->setSource(new Source\File($file)); + $adapter->deinit(); + + if ($actual) { + return $file; + } + + return $this->getFilename($file); + } + + /** + * Gets the hash. + * + * @param string $type + * @param int $quality + * @param array $extras + * @return string + */ + public function getHash($type = 'guess', $quality = 80, $extras = []) + { + if (null === $this->hash) { + $this->generateHash($type, $quality, $extras); + } + + return $this->hash; + } + + /** + * Generates the hash. + * + * @param string $type + * @param int $quality + * @param array $extras + */ + public function generateHash($type = 'guess', $quality = 80, $extras = []) + { + $inputInfos = $this->source->getInfos(); + + $data = [ + $inputInfos, + $this->serializeOperations(), + $type, + $quality, + $extras + ]; + + $this->hash = sha1(serialize($data)); + } + + /** + * Read exif rotation from file and apply it. + */ + public function fixOrientation() + { + if (!extension_loaded('exif')) { + throw new RuntimeException('You need to EXIF PHP Extension to use this function'); + } + + if (!in_array(exif_imagetype($this->source->getInfos()), [IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM], true)) { + return $this; + } + + // resolve any streams + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $filepath = $this->source->getInfos(); + if ($locator->isStream($filepath)) { + $filepath = $locator->findResource($this->source->getInfos(), true, true); + } + + // Make sure file exists + if (!file_exists($filepath)) { + return $this; + } + + try { + $exif = @exif_read_data($filepath); + } catch (Exception $e) { + Grav::instance()['log']->error($filepath . ' - ' . $e->getMessage()); + return $this; + } + + if ($exif === false || !array_key_exists('Orientation', $exif)) { + return $this; + } + + return $this->applyExifOrientation($exif['Orientation']); + } +} diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php new file mode 100644 index 0000000..46c832e --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php @@ -0,0 +1,477 @@ +getGrav()['config']; + + $this->thumbnailTypes = ['page', 'media', 'default']; + $this->default_quality = $config->get('system.images.default_image_quality', 85); + $this->def('debug', $config->get('system.images.debug')); + + $path = $this->get('filepath'); + if (!$path || !file_exists($path) || !filesize($path)) { + return; + } + + $this->set('thumbnails.media', $path); + + if (!($this->offsetExists('width') && $this->offsetExists('height') && $this->offsetExists('mime'))) { + $image_info = getimagesize($path); + if ($image_info) { + $this->def('width', $image_info[0]); + $this->def('height', $image_info[1]); + $this->def('mime', $image_info['mime']); + } + } + + $this->reset(); + + if ($config->get('system.images.cache_all', false)) { + $this->cache(); + } + } + + /** + * @return array + */ + public function getMeta(): array + { + return [ + 'width' => $this->width, + 'height' => $this->height, + ] + parent::getMeta(); + } + + /** + * Also unset the image on destruct. + */ + public function __destruct() + { + unset($this->image); + } + + /** + * Also clone image. + */ + public function __clone() + { + if ($this->image) { + $this->image = clone $this->image; + } + + parent::__clone(); + } + + /** + * Reset image. + * + * @return $this + */ + public function reset() + { + parent::reset(); + + if ($this->image) { + $this->image(); + $this->medium_querystring = []; + $this->filter(); + $this->clearAlternatives(); + } + + $this->format = 'guess'; + $this->quality = $this->default_quality; + + $this->debug_watermarked = false; + + $config = $this->getGrav()['config']; + // Set CLS configuration + $this->auto_sizes = $config->get('system.images.cls.auto_sizes', false); + $this->aspect_ratio = $config->get('system.images.cls.aspect_ratio', false); + $this->retina_scale = $config->get('system.images.cls.retina_scale', 1); + + return $this; + } + + /** + * Add meta file for the medium. + * + * @param string $filepath + * @return $this + */ + public function addMetaFile($filepath) + { + parent::addMetaFile($filepath); + + // Apply filters in meta file + $this->reset(); + + return $this; + } + + /** + * Return PATH to image. + * + * @param bool $reset + * @return string path to image + */ + public function path($reset = true) + { + $output = $this->saveImage(); + + if ($reset) { + $this->reset(); + } + + return $output; + } + + /** + * Return URL to image. + * + * @param bool $reset + * @return string + */ + public function url($reset = true) + { + $grav = $this->getGrav(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + $image_path = (string)($locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true)); + $saved_image_path = $this->saved_image_path = $this->saveImage(); + + $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path) ?: $saved_image_path; + + if ($locator->isStream($output)) { + $output = (string)($locator->findResource($output, false) ?: $locator->findResource($output, false, true)); + } + + if (Utils::startsWith($output, $image_path)) { + $image_dir = $locator->findResource('cache://images', false); + $output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output); + } + + if ($reset) { + $this->reset(); + } + + return trim($grav['base_url'] . '/' . $this->urlQuerystring($output), '\\'); + } + + /** + * Return srcset string for this Medium and its alternatives. + * + * @param bool $reset + * @return string + */ + public function srcset($reset = true) + { + if (empty($this->alternatives)) { + if ($reset) { + $this->reset(); + } + + return ''; + } + + $srcset = []; + foreach ($this->alternatives as $ratio => $medium) { + $srcset[] = $medium->url($reset) . ' ' . $medium->get('width') . 'w'; + } + $srcset[] = str_replace(' ', '%20', $this->url($reset)) . ' ' . $this->get('width') . 'w'; + + return implode(', ', $srcset); + } + + /** + * Parsedown element for source display mode + * + * @param array $attributes + * @param bool $reset + * @return array + */ + public function sourceParsedownElement(array $attributes, $reset = true) + { + empty($attributes['src']) && $attributes['src'] = $this->url(false); + + $srcset = $this->srcset($reset); + if ($srcset) { + empty($attributes['srcset']) && $attributes['srcset'] = $srcset; + $attributes['sizes'] = $this->sizes(); + } + + if ($this->saved_image_path && $this->auto_sizes) { + if (!array_key_exists('height', $this->attributes) && !array_key_exists('width', $this->attributes)) { + $info = getimagesize($this->saved_image_path); + $width = intval($info[0]); + $height = intval($info[1]); + + $scaling_factor = $this->retina_scale > 0 ? $this->retina_scale : 1; + $attributes['width'] = intval($width / $scaling_factor); + $attributes['height'] = intval($height / $scaling_factor); + + if ($this->aspect_ratio) { + $style = ($attributes['style'] ?? ' ') . "--aspect-ratio: $width/$height;"; + $attributes['style'] = trim($style); + } + } + } + + return ['name' => 'img', 'attributes' => $attributes]; + } + + /** + * Turn the current Medium into a Link + * + * @param bool $reset + * @param array $attributes + * @return MediaLinkInterface + */ + public function link($reset = true, array $attributes = []) + { + $attributes['href'] = $this->url(false); + $srcset = $this->srcset(false); + if ($srcset) { + $attributes['data-srcset'] = $srcset; + } + + return parent::link($reset, $attributes); + } + + /** + * Turn the current Medium into a Link with lightbox enabled + * + * @param int $width + * @param int $height + * @param bool $reset + * @return MediaLinkInterface + */ + public function lightbox($width = null, $height = null, $reset = true) + { + if ($this->mode !== 'source') { + $this->display('source'); + } + + if ($width && $height) { + $this->__call('cropResize', [$width, $height]); + } + + return parent::lightbox($width, $height, $reset); + } + + public function autoSizes($enabled = 'true') + { + $enabled = $enabled === 'true' ?: false; + $this->auto_sizes = $enabled; + + return $this; + } + + public function aspectRatio($enabled = 'true') + { + $enabled = $enabled === 'true' ?: false; + $this->aspect_ratio = $enabled; + + return $this; + } + + public function retinaScale($scale = 1) + { + $this->retina_scale = intval($scale); + + return $this; + } + + public function watermark($image = null, $position = null, $scale = null) + { + $grav = $this->getGrav(); + + $locator = $grav['locator']; + $config = $grav['config']; + + $args = func_get_args(); + + $file = $args[0] ?? '1'; // using '1' because of markdown. doing ![](image.jpg?watermark) returns $args[0]='1'; + $file = $file === '1' ? $config->get('system.images.watermark.image') : $args[0]; + + $watermark = $locator->findResource($file); + $watermark = ImageFile::open($watermark); + + // Scaling operations + $scale = ($scale ?? $config->get('system.images.watermark.scale', 100)) / 100; + $wwidth = $this->get('width') * $scale; + $wheight = $this->get('height') * $scale; + $watermark->resize($wwidth, $wheight); + + // Position operations + $position = !empty($args[1]) ? explode('-', $args[1]) : ['center', 'center']; // todo change to config + $positionY = $position[0] ?? $config->get('system.images.watermark.position_y', 'center'); + $positionX = $position[1] ?? $config->get('system.images.watermark.position_x', 'center'); + + switch ($positionY) + { + case 'top': + $positionY = 0; + break; + + case 'bottom': + $positionY = $this->get('height')-$wheight; + break; + + case 'center': + $positionY = ($this->get('height')/2) - ($wheight/2); + break; + } + + switch ($positionX) + { + case 'left': + $positionX = 0; + break; + + case 'right': + $positionX = $this->get('width')-$wwidth; + break; + + case 'center': + $positionX = ($this->get('width')/2) - ($wwidth/2); + break; + } + + $this->__call('merge', [$watermark,$positionX, $positionY]); + + return $this; + } + + /** + * Handle this commonly used variant + * + * @return $this + */ + public function cropZoom() + { + $this->__call('zoomCrop', func_get_args()); + + return $this; + } + + /** + * Add a frame to image + * + * @return $this + */ + public function addFrame(int $border = 10, string $color = '0x000000') + { + if(is_int(intval($border)) && $border>0 && preg_match('/^0x[a-f0-9]{6}$/i', $color)) { // $border must be an integer and bigger than 0; $color must be formatted as an HEX value (0x??????). + $image = ImageFile::open($this->path()); + } + else { + return $this; + } + + $dst_width = $image->width()+2*$border; + $dst_height = $image->height()+2*$border; + + $frame = ImageFile::create($dst_width, $dst_height); + + $frame->__call('fill', [$color]); + + $this->image = $frame; + + $this->__call('merge', [$image, $border, $border]); + + $this->saveImage(); + + return $this; + + } + + /** + * Forward the call to the image processing method. + * + * @param string $method + * @param mixed $args + * @return $this|mixed + */ + + public function __call($method, $args) + { + if (!in_array($method, static::$magic_actions, true)) { + return parent::__call($method, $args); + } + + // Always initialize image. + if (!$this->image) { + $this->image(); + } + + try { + $this->image->{$method}(...$args); + + /** @var ImageMediaInterface $medium */ + foreach ($this->alternatives as $medium) { + $args_copy = $args; + + // regular image: resize 400x400 -> 200x200 + // --> @2x: resize 800x800->400x400 + if (isset(static::$magic_resize_actions[$method])) { + foreach (static::$magic_resize_actions[$method] as $param) { + if (isset($args_copy[$param])) { + $args_copy[$param] *= $medium->get('ratio'); + } + } + } + + // Do the same call for alternative media. + $medium->__call($method, $args_copy); + } + } catch (BadFunctionCallException $e) { + } + + return $this; + } +} diff --git a/system/src/Grav/Common/Page/Medium/Link.php b/system/src/Grav/Common/Page/Medium/Link.php new file mode 100644 index 0000000..d576875 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/Link.php @@ -0,0 +1,101 @@ +attributes = $attributes; + + $source = $medium->reset()->thumbnail('auto')->display('thumbnail'); + if (!$source instanceof MediaObjectInterface) { + throw new RuntimeException('Media has no thumbnail set'); + } + + $source->set('linked', true); + + $this->source = $source; + } + + /** + * Get an element (is array) that can be rendered by the Parsedown engine + * + * @param string|null $title + * @param string|null $alt + * @param string|null $class + * @param string|null $id + * @param bool $reset + * @return array + */ + public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true) + { + $innerElement = $this->source->parsedownElement($title, $alt, $class, $id, $reset); + + return [ + 'name' => 'a', + 'attributes' => $this->attributes, + 'handler' => is_array($innerElement) ? 'element' : 'line', + 'text' => $innerElement + ]; + } + + /** + * Forward the call to the source element + * + * @param string $method + * @param mixed $args + * @return mixed + */ + public function __call($method, $args) + { + $object = $this->source; + $callable = [$object, $method]; + if (!is_callable($callable)) { + throw new BadMethodCallException(get_class($object) . '::' . $method . '() not found.'); + } + + $object = call_user_func_array($callable, $args); + if (!$object instanceof MediaLinkInterface) { + // Don't start nesting links, if user has multiple link calls in his + // actions, we will drop the previous links. + return $this; + } + + $this->source = $object; + + return $object; + } +} diff --git a/system/src/Grav/Common/Page/Medium/Medium.php b/system/src/Grav/Common/Page/Medium/Medium.php new file mode 100644 index 0000000..87009ac --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/Medium.php @@ -0,0 +1,130 @@ +get('system.media.enable_media_timestamp', true)) { + $this->timestamp = Grav::instance()['cache']->getKey(); + } + + $this->def('mime', 'application/octet-stream'); + + if (!$this->offsetExists('size')) { + $path = $this->get('filepath'); + $this->def('size', filesize($path)); + } + + $this->reset(); + } + + /** + * Clone medium. + */ + public function __clone() + { + // Allows future compatibility as parent::__clone() works. + } + + /** + * Add meta file for the medium. + * + * @param string $filepath + */ + public function addMetaFile($filepath) + { + $this->metadata = (array)CompiledYamlFile::instance($filepath)->content(); + $this->merge($this->metadata); + } + + /** + * @return array + */ + public function getMeta(): array + { + return [ + 'mime' => $this->mime, + 'size' => $this->size, + 'modified' => $this->modified, + ]; + } + + /** + * Return string representation of the object (html). + * + * @return string + */ + public function __toString() + { + return $this->html(); + } + + /** + * @param string $thumb + */ + protected function createThumbnail($thumb) + { + return MediumFactory::fromFile($thumb, ['type' => 'thumbnail']); + } + + /** + * @param array $attributes + * @return MediaLinkInterface + */ + protected function createLink(array $attributes) + { + return new Link($attributes, $this); + } + + /** + * @return Grav + */ + protected function getGrav(): Grav + { + return Grav::instance(); + } + + /** + * @return array + */ + protected function getItems(): array + { + return $this->items; + } +} diff --git a/system/src/Grav/Common/Page/Medium/MediumFactory.php b/system/src/Grav/Common/Page/Medium/MediumFactory.php new file mode 100644 index 0000000..7dee4ea --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/MediumFactory.php @@ -0,0 +1,216 @@ +get('media.types.' . strtolower($ext)) : null; + if (!is_array($media_params)) { + return null; + } + + // Remove empty 'image' attribute + if (isset($media_params['image']) && empty($media_params['image'])) { + unset($media_params['image']); + } + + $params += $media_params; + + // Add default settings for undefined variables. + $params += (array)$config->get('media.types.defaults'); + $params += [ + 'type' => 'file', + 'thumb' => 'media/thumb.png', + 'mime' => 'application/octet-stream', + 'filepath' => $file, + 'filename' => $filename, + 'basename' => $basename, + 'extension' => $ext, + 'path' => $path, + 'modified' => filemtime($file), + 'thumbnails' => [] + ]; + + $locator = Grav::instance()['locator']; + + $file = $locator->findResource("image://{$params['thumb']}"); + if ($file) { + $params['thumbnails']['default'] = $file; + } + + return static::fromArray($params); + } + + /** + * Create Medium from an uploaded file + * + * @param UploadedFileInterface $uploadedFile + * @param array $params + * @return Medium|null + */ + public static function fromUploadedFile(UploadedFileInterface $uploadedFile, array $params = []) + { + // For now support only FormFlashFiles, which exist over multiple requests. Also ignore errored and moved media. + if (!$uploadedFile instanceof FormFlashFile || $uploadedFile->getError() !== \UPLOAD_ERR_OK || $uploadedFile->isMoved()) { + return null; + } + + $clientName = $uploadedFile->getClientFilename(); + if (!$clientName) { + return null; + } + + $parts = pathinfo($clientName); + $filename = $parts['basename']; + $ext = $parts['extension'] ?? ''; + $basename = $parts['filename']; + $file = $uploadedFile->getTmpFile(); + $path = $file ? dirname($file) : ''; + + $config = Grav::instance()['config']; + + $media_params = $ext ? $config->get('media.types.' . strtolower($ext)) : null; + if (!is_array($media_params)) { + return null; + } + + $params += $media_params; + + // Add default settings for undefined variables. + $params += (array)$config->get('media.types.defaults'); + $params += [ + 'type' => 'file', + 'thumb' => 'media/thumb.png', + 'mime' => 'application/octet-stream', + 'filepath' => $file, + 'filename' => $filename, + 'basename' => $basename, + 'extension' => $ext, + 'path' => $path, + 'modified' => $file ? filemtime($file) : 0, + 'thumbnails' => [] + ]; + + $locator = Grav::instance()['locator']; + + $file = $locator->findResource("image://{$params['thumb']}"); + if ($file) { + $params['thumbnails']['default'] = $file; + } + + return static::fromArray($params); + } + + /** + * Create Medium from array of parameters + * + * @param array $items + * @param Blueprint|null $blueprint + * @return Medium + */ + public static function fromArray(array $items = [], Blueprint $blueprint = null) + { + $type = $items['type'] ?? null; + + switch ($type) { + case 'image': + return new ImageMedium($items, $blueprint); + case 'thumbnail': + return new ThumbnailImageMedium($items, $blueprint); + case 'animated': + case 'vector': + return new StaticImageMedium($items, $blueprint); + case 'video': + return new VideoMedium($items, $blueprint); + case 'audio': + return new AudioMedium($items, $blueprint); + default: + return new Medium($items, $blueprint); + } + } + + /** + * Create a new ImageMedium by scaling another ImageMedium object. + * + * @param ImageMediaInterface|MediaObjectInterface $medium + * @param int $from + * @param int $to + * @return ImageMediaInterface|MediaObjectInterface|array + */ + public static function scaledFromMedium($medium, $from, $to) + { + if (!$medium instanceof ImageMedium) { + return $medium; + } + + if ($to > $from) { + return $medium; + } + + $ratio = $to / $from; + $width = $medium->get('width') * $ratio; + $height = $medium->get('height') * $ratio; + + $prev_basename = $medium->get('basename'); + $basename = str_replace('@'.$from.'x', '@'.$to.'x', $prev_basename); + + $debug = $medium->get('debug'); + $medium->set('debug', false); + $medium->setImagePrettyName($basename); + + $file = $medium->resize($width, $height)->path(); + + $medium->set('debug', $debug); + $medium->setImagePrettyName($prev_basename); + + $size = filesize($file); + + $medium = self::fromFile($file); + if ($medium) { + $medium->set('size', $size); + } + + return ['file' => $medium, 'size' => $size]; + } +} diff --git a/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php new file mode 100644 index 0000000..44ab952 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php @@ -0,0 +1,44 @@ +parsedownElement($title, $alt, $class, $id, $reset); + + if (!$this->parsedown) { + $this->parsedown = new Parsedown(new Excerpts()); + } + + return $this->parsedown->elementToHtml($element); + } +} diff --git a/system/src/Grav/Common/Page/Medium/RenderableInterface.php b/system/src/Grav/Common/Page/Medium/RenderableInterface.php new file mode 100644 index 0000000..ac72447 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/RenderableInterface.php @@ -0,0 +1,41 @@ +url($reset); + } + + return ['name' => 'img', 'attributes' => $attributes]; + } +} diff --git a/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php b/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php new file mode 100644 index 0000000..d95b2d6 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php @@ -0,0 +1,24 @@ +resetPlayer(); + + return $this; + } +} diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php new file mode 100644 index 0000000..9e68a44 --- /dev/null +++ b/system/src/Grav/Common/Page/Page.php @@ -0,0 +1,2900 @@ +taxonomy = []; + $this->process = $config->get('system.pages.process'); + $this->published = true; + } + + /** + * Initializes the page instance variables based on a file + * + * @param SplFileInfo $file The file information for the .md file that the page represents + * @param string|null $extension + * @return $this + */ + public function init(SplFileInfo $file, $extension = null) + { + $config = Grav::instance()['config']; + + $this->initialized = true; + + // some extension logic + if (empty($extension)) { + $this->extension('.' . $file->getExtension()); + } else { + $this->extension($extension); + } + + // extract page language from page extension + $language = trim(basename($this->extension(), 'md'), '.') ?: null; + $this->language($language); + + $this->hide_home_route = $config->get('system.home.hide_in_urls', false); + $this->home_route = $this->adjustRouteCase($config->get('system.home.alias')); + $this->filePath($file->getPathname()); + $this->modified($file->getMTime()); + $this->id($this->modified() . md5($this->filePath())); + $this->routable(true); + $this->header(); + $this->date(); + $this->metadata(); + $this->url(); + $this->visible(); + $this->modularTwig(strpos($this->slug(), '_') === 0); + $this->setPublishState(); + $this->published(); + $this->urlExtension(); + + return $this; + } + + public function __clone() + { + $this->initialized = false; + $this->header = $this->header ? clone $this->header : null; + } + + /** + * @return void + */ + public function initialize(): void + { + if (!$this->initialized) { + $this->initialized = true; + $this->route = null; + $this->raw_route = null; + $this->_forms = null; + } + } + + /** + * @return void + */ + protected function processFrontmatter() + { + // Quick check for twig output tags in frontmatter if enabled + $process_fields = (array)$this->header(); + if (Utils::contains(json_encode(array_values($process_fields)), '{{')) { + $ignored_fields = []; + foreach ((array)Grav::instance()['config']->get('system.pages.frontmatter.ignore_fields') as $field) { + if (isset($process_fields[$field])) { + $ignored_fields[$field] = $process_fields[$field]; + unset($process_fields[$field]); + } + } + $text_header = Grav::instance()['twig']->processString(json_encode($process_fields, JSON_UNESCAPED_UNICODE), ['page' => $this]); + $this->header((object)(json_decode($text_header, true) + $ignored_fields)); + } + } + + /** + * Return an array with the routes of other translated languages + * + * @param bool $onlyPublished only return published translations + * @return array the page translated languages + */ + public function translatedLanguages($onlyPublished = false) + { + $grav = Grav::instance(); + + /** @var Language $language */ + $language = $grav['language']; + + $languages = $language->getLanguages(); + $defaultCode = $language->getDefault(); + + $name = substr($this->name, 0, -strlen($this->extension())); + $translatedLanguages = []; + + foreach ($languages as $languageCode) { + $languageExtension = ".{$languageCode}.md"; + $path = $this->path . DS . $this->folder . DS . $name . $languageExtension; + $exists = file_exists($path); + + // Default language may be saved without language file location. + if (!$exists && $languageCode === $defaultCode) { + $languageExtension = '.md'; + $path = $this->path . DS . $this->folder . DS . $name . $languageExtension; + $exists = file_exists($path); + } + + if ($exists) { + $aPage = new Page(); + $aPage->init(new SplFileInfo($path), $languageExtension); + $aPage->route($this->route()); + $aPage->rawRoute($this->rawRoute()); + $route = $aPage->header()->routes['default'] ?? $aPage->rawRoute(); + if (!$route) { + $route = $aPage->route(); + } + + if ($onlyPublished && !$aPage->published()) { + continue; + } + + $translatedLanguages[$languageCode] = $route; + } + } + + return $translatedLanguages; + } + + /** + * Return an array listing untranslated languages available + * + * @param bool $includeUnpublished also list unpublished translations + * @return array the page untranslated languages + */ + public function untranslatedLanguages($includeUnpublished = false) + { + $grav = Grav::instance(); + + /** @var Language $language */ + $language = $grav['language']; + + $languages = $language->getLanguages(); + $translated = array_keys($this->translatedLanguages(!$includeUnpublished)); + + return array_values(array_diff($languages, $translated)); + } + + /** + * Gets and Sets the raw data + * + * @param string|null $var Raw content string + * @return string Raw content string + */ + public function raw($var = null) + { + $file = $this->file(); + + if ($var) { + // First update file object. + if ($file) { + $file->raw($var); + } + + // Reset header and content. + $this->modified = time(); + $this->id($this->modified() . md5($this->filePath())); + $this->header = null; + $this->content = null; + $this->summary = null; + } + + return $file ? $file->raw() : ''; + } + + /** + * Gets and Sets the page frontmatter + * + * @param string|null $var + * + * @return string + */ + public function frontmatter($var = null) + { + if ($var) { + $this->frontmatter = (string)$var; + + // Update also file object. + $file = $this->file(); + if ($file) { + $file->frontmatter((string)$var); + } + + // Force content re-processing. + $this->id(time() . md5($this->filePath())); + } + if (!$this->frontmatter) { + $this->header(); + } + + return $this->frontmatter; + } + + /** + * Gets and Sets the header based on the YAML configuration at the top of the .md file + * + * @param object|array|null $var a YAML object representing the configuration for the file + * @return object the current YAML configuration + */ + public function header($var = null) + { + if ($var) { + $this->header = (object)$var; + + // Update also file object. + $file = $this->file(); + if ($file) { + $file->header((array)$var); + } + + // Force content re-processing. + $this->id(time() . md5($this->filePath())); + } + if (!$this->header) { + $file = $this->file(); + if ($file) { + try { + $this->raw_content = $file->markdown(); + $this->frontmatter = $file->frontmatter(); + $this->header = (object)$file->header(); + + if (!Utils::isAdminPlugin()) { + // If there's a `frontmatter.yaml` file merge that in with the page header + // note page's own frontmatter has precedence and will overwrite any defaults + $frontmatterFile = CompiledYamlFile::instance($this->path . '/' . $this->folder . '/frontmatter.yaml'); + if ($frontmatterFile->exists()) { + $frontmatter_data = (array)$frontmatterFile->content(); + $this->header = (object)array_replace_recursive( + $frontmatter_data, + (array)$this->header + ); + $frontmatterFile->free(); + } + // Process frontmatter with Twig if enabled + if (Grav::instance()['config']->get('system.pages.frontmatter.process_twig') === true) { + $this->processFrontmatter(); + } + } + } catch (Exception $e) { + $file->raw(Grav::instance()['language']->translate([ + 'GRAV.FRONTMATTER_ERROR_PAGE', + $this->slug(), + $file->filename(), + $e->getMessage(), + $file->raw() + ])); + $this->raw_content = $file->markdown(); + $this->frontmatter = $file->frontmatter(); + $this->header = (object)$file->header(); + } + $var = true; + } + } + + if ($var) { + if (isset($this->header->slug)) { + $this->slug($this->header->slug); + } + if (isset($this->header->routes)) { + $this->routes = (array)$this->header->routes; + } + if (isset($this->header->title)) { + $this->title = trim($this->header->title); + } + if (isset($this->header->language)) { + $this->language = trim($this->header->language); + } + if (isset($this->header->template)) { + $this->template = trim($this->header->template); + } + if (isset($this->header->menu)) { + $this->menu = trim($this->header->menu); + } + if (isset($this->header->routable)) { + $this->routable = (bool)$this->header->routable; + } + if (isset($this->header->visible)) { + $this->visible = (bool)$this->header->visible; + } + if (isset($this->header->redirect)) { + $this->redirect = trim($this->header->redirect); + } + if (isset($this->header->external_url)) { + $this->external_url = trim($this->header->external_url); + } + if (isset($this->header->order_dir)) { + $this->order_dir = trim($this->header->order_dir); + } + if (isset($this->header->order_by)) { + $this->order_by = trim($this->header->order_by); + } + if (isset($this->header->order_manual)) { + $this->order_manual = (array)$this->header->order_manual; + } + if (isset($this->header->dateformat)) { + $this->dateformat($this->header->dateformat); + } + if (isset($this->header->date)) { + $this->date($this->header->date); + } + if (isset($this->header->markdown_extra)) { + $this->markdown_extra = (bool)$this->header->markdown_extra; + } + if (isset($this->header->taxonomy)) { + $this->taxonomy($this->header->taxonomy); + } + if (isset($this->header->max_count)) { + $this->max_count = (int)$this->header->max_count; + } + if (isset($this->header->process)) { + foreach ((array)$this->header->process as $process => $status) { + $this->process[$process] = (bool)$status; + } + } + if (isset($this->header->published)) { + $this->published = (bool)$this->header->published; + } + if (isset($this->header->publish_date)) { + $this->publishDate($this->header->publish_date); + } + if (isset($this->header->unpublish_date)) { + $this->unpublishDate($this->header->unpublish_date); + } + if (isset($this->header->expires)) { + $this->expires = (int)$this->header->expires; + } + if (isset($this->header->cache_control)) { + $this->cache_control = $this->header->cache_control; + } + if (isset($this->header->etag)) { + $this->etag = (bool)$this->header->etag; + } + if (isset($this->header->last_modified)) { + $this->last_modified = (bool)$this->header->last_modified; + } + if (isset($this->header->ssl)) { + $this->ssl = (bool)$this->header->ssl; + } + if (isset($this->header->template_format)) { + $this->template_format = $this->header->template_format; + } + if (isset($this->header->debugger)) { + $this->debugger = (bool)$this->header->debugger; + } + if (isset($this->header->append_url_extension)) { + $this->url_extension = $this->header->append_url_extension; + } + } + + return $this->header; + } + + /** + * Get page language + * + * @param string|null $var + * @return mixed + */ + public function language($var = null) + { + if ($var !== null) { + $this->language = $var; + } + + return $this->language; + } + + /** + * Modify a header value directly + * + * @param string $key + * @param mixed $value + */ + public function modifyHeader($key, $value) + { + $this->header->{$key} = $value; + } + + /** + * @return int + */ + public function httpResponseCode() + { + return (int)($this->header()->http_response_code ?? 200); + } + + /** + * @return array + */ + public function httpHeaders() + { + $headers = []; + + $grav = Grav::instance(); + $format = $this->templateFormat(); + $cache_control = $this->cacheControl(); + $expires = $this->expires(); + + // Set Content-Type header + $headers['Content-Type'] = Utils::getMimeByExtension($format, 'text/html'); + + // Calculate Expires Headers if set to > 0 + if ($expires > 0) { + $expires_date = gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT'; + if (!$cache_control) { + $headers['Cache-Control'] = 'max-age=' . $expires; + } + $headers['Expires'] = $expires_date; + } + + // Set Cache-Control header + if ($cache_control) { + $headers['Cache-Control'] = strtolower($cache_control); + } + + // Set Last-Modified header + if ($this->lastModified()) { + $last_modified_date = gmdate('D, d M Y H:i:s', $this->modified()) . ' GMT'; + $headers['Last-Modified'] = $last_modified_date; + } + + // Ask Grav to calculate ETag from the final content. + if ($this->eTag()) { + $headers['ETag'] = '1'; + } + + // Set Vary: Accept-Encoding header + if ($grav['config']->get('system.pages.vary_accept_encoding', false)) { + $headers['Vary'] = 'Accept-Encoding'; + } + + return $headers; + } + + /** + * Get the summary. + * + * @param int|null $size Max summary size. + * @param bool $textOnly Only count text size. + * @return string + */ + public function summary($size = null, $textOnly = false) + { + $config = (array)Grav::instance()['config']->get('site.summary'); + if (isset($this->header->summary)) { + $config = array_merge($config, $this->header->summary); + } + + // Return summary based on settings in site config file + if (!$config['enabled']) { + return $this->content(); + } + + // Set up variables to process summary from page or from custom summary + if ($this->summary === null) { + $content = $textOnly ? strip_tags($this->content()) : $this->content(); + $summary_size = $this->summary_size; + } else { + $content = $textOnly ? strip_tags($this->summary) : $this->summary; + $summary_size = mb_strwidth($content, 'utf-8'); + } + + // Return calculated summary based on summary divider's position + $format = $config['format']; + // Return entire page content on wrong/ unknown format + if (!in_array($format, ['short', 'long'])) { + return $content; + } + if (($format === 'short') && isset($summary_size)) { + // Slice the string + if (mb_strwidth($content, 'utf8') > $summary_size) { + return mb_substr($content, 0, $summary_size); + } + + return $content; + } + + // Get summary size from site config's file + if ($size === null) { + $size = $config['size']; + } + + // If the size is zero, return the entire page content + if ($size === 0) { + return $content; + // Return calculated summary based on defaults + } + if (!is_numeric($size) || ($size < 0)) { + $size = 300; + } + + // Only return string but not html, wrap whatever html tag you want when using + if ($textOnly) { + if (mb_strwidth($content, 'utf-8') <= $size) { + return $content; + } + + return mb_strimwidth($content, 0, $size, '…', 'UTF-8'); + } + + $summary = Utils::truncateHtml($content, $size); + + return html_entity_decode($summary, ENT_COMPAT | ENT_HTML401, 'UTF-8'); + } + + /** + * Sets the summary of the page + * + * @param string $summary Summary + */ + public function setSummary($summary) + { + $this->summary = $summary; + } + + /** + * Gets and Sets the content based on content portion of the .md file + * + * @param string|null $var Content + * @return string Content + */ + public function content($var = null) + { + if ($var !== null) { + $this->raw_content = $var; + + // Update file object. + $file = $this->file(); + if ($file) { + $file->markdown($var); + } + + // Force re-processing. + $this->id(time() . md5($this->filePath())); + $this->content = null; + } + // If no content, process it + if ($this->content === null) { + // Get media + $this->media(); + + /** @var Config $config */ + $config = Grav::instance()['config']; + + // Load cached content + /** @var Cache $cache */ + $cache = Grav::instance()['cache']; + $cache_id = md5('page' . $this->getCacheKey()); + $content_obj = $cache->fetch($cache_id); + + if (is_array($content_obj)) { + $this->content = $content_obj['content']; + $this->content_meta = $content_obj['content_meta']; + } else { + $this->content = $content_obj; + } + + + $process_markdown = $this->shouldProcess('markdown'); + $process_twig = $this->shouldProcess('twig') || $this->modularTwig(); + + $cache_enable = $this->header->cache_enable ?? $config->get( + 'system.cache.enabled', + true + ); + $twig_first = $this->header->twig_first ?? $config->get( + 'system.pages.twig_first', + false + ); + + // never cache twig means it's always run after content + $never_cache_twig = $this->header->never_cache_twig ?? $config->get( + 'system.pages.never_cache_twig', + true + ); + + // if no cached-content run everything + if ($never_cache_twig) { + if ($this->content === false || $cache_enable === false) { + $this->content = $this->raw_content; + Grav::instance()->fireEvent('onPageContentRaw', new Event(['page' => $this])); + + if ($process_markdown) { + $this->processMarkdown(); + } + + // Content Processed but not cached yet + Grav::instance()->fireEvent('onPageContentProcessed', new Event(['page' => $this])); + + if ($cache_enable) { + $this->cachePageContent(); + } + } + + if ($process_twig) { + $this->processTwig(); + } + } else { + if ($this->content === false || $cache_enable === false) { + $this->content = $this->raw_content; + Grav::instance()->fireEvent('onPageContentRaw', new Event(['page' => $this])); + + if ($twig_first) { + if ($process_twig) { + $this->processTwig(); + } + if ($process_markdown) { + $this->processMarkdown(); + } + + // Content Processed but not cached yet + Grav::instance()->fireEvent('onPageContentProcessed', new Event(['page' => $this])); + } else { + if ($process_markdown) { + $this->processMarkdown($process_twig); + } + + // Content Processed but not cached yet + Grav::instance()->fireEvent('onPageContentProcessed', new Event(['page' => $this])); + + if ($process_twig) { + $this->processTwig(); + } + } + + if ($cache_enable) { + $this->cachePageContent(); + } + } + } + + // Handle summary divider + $delimiter = $config->get('site.summary.delimiter', '==='); + $divider_pos = mb_strpos($this->content, "

{$delimiter}

"); + if ($divider_pos !== false) { + $this->summary_size = $divider_pos; + $this->content = str_replace("

{$delimiter}

", '', $this->content); + } + + // Fire event when Page::content() is called + Grav::instance()->fireEvent('onPageContent', new Event(['page' => $this])); + } + + return $this->content; + } + + /** + * Get the contentMeta array and initialize content first if it's not already + * + * @return mixed + */ + public function contentMeta() + { + if ($this->content === null) { + $this->content(); + } + + return $this->getContentMeta(); + } + + /** + * Add an entry to the page's contentMeta array + * + * @param string $name + * @param mixed $value + */ + public function addContentMeta($name, $value) + { + $this->content_meta[$name] = $value; + } + + /** + * Return the whole contentMeta array as it currently stands + * + * @param string|null $name + * + * @return mixed|null + */ + public function getContentMeta($name = null) + { + if ($name) { + return $this->content_meta[$name] ?? null; + } + + return $this->content_meta; + } + + /** + * Sets the whole content meta array in one shot + * + * @param array $content_meta + * + * @return array + */ + public function setContentMeta($content_meta) + { + return $this->content_meta = $content_meta; + } + + /** + * Process the Markdown content. Uses Parsedown or Parsedown Extra depending on configuration + * + * @param bool $keepTwig If true, content between twig tags will not be processed. + * @return void + */ + protected function processMarkdown(bool $keepTwig = false) + { + /** @var Config $config */ + $config = Grav::instance()['config']; + + $markdownDefaults = (array)$config->get('system.pages.markdown'); + if (isset($this->header()->markdown)) { + $markdownDefaults = array_merge($markdownDefaults, $this->header()->markdown); + } + + // pages.markdown_extra is deprecated, but still check it... + if (!isset($markdownDefaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) { + user_error('Configuration option \'system.pages.markdown_extra\' is deprecated since Grav 1.5, use \'system.pages.markdown.extra\' instead', E_USER_DEPRECATED); + + $markdownDefaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra'); + } + + $extra = $markdownDefaults['extra'] ?? false; + $defaults = [ + 'markdown' => $markdownDefaults, + 'images' => $config->get('system.images', []) + ]; + + $excerpts = new Excerpts($this, $defaults); + + // Initialize the preferred variant of Parsedown + if ($extra) { + $parsedown = new ParsedownExtra($excerpts); + } else { + $parsedown = new Parsedown($excerpts); + } + + $content = $this->content; + if ($keepTwig) { + $token = [ + '/' . Utils::generateRandomString(3), + Utils::generateRandomString(3) . '/' + ]; + // Base64 encode any twig. + $content = preg_replace_callback( + ['/({#.*?#})/mu', '/({{.*?}})/mu', '/({%.*?%})/mu'], + static function ($matches) use ($token) { return $token[0] . base64_encode($matches[1]) . $token[1]; }, + $content + ); + } + + $content = $parsedown->text($content); + + if ($keepTwig) { + // Base64 decode the encoded twig. + $content = preg_replace_callback( + ['`' . $token[0] . '([A-Za-z0-9+/]+={0,2})' . $token[1] . '`mu'], + static function ($matches) { return base64_decode($matches[1]); }, + $content + ); + } + + $this->content = $content; + } + + + /** + * Process the Twig page content. + * + * @return void + */ + private function processTwig() + { + /** @var Twig $twig */ + $twig = Grav::instance()['twig']; + $this->content = $twig->processPage($this, $this->content); + } + + /** + * Fires the onPageContentProcessed event, and caches the page content using a unique ID for the page + * + * @return void + */ + public function cachePageContent() + { + /** @var Cache $cache */ + $cache = Grav::instance()['cache']; + $cache_id = md5('page' . $this->getCacheKey()); + $cache->save($cache_id, ['content' => $this->content, 'content_meta' => $this->content_meta]); + } + + /** + * Needed by the onPageContentProcessed event to get the raw page content + * + * @return string the current page content + */ + public function getRawContent() + { + return $this->content; + } + + /** + * Needed by the onPageContentProcessed event to set the raw page content + * + * @param string|null $content + * @return void + */ + public function setRawContent($content) + { + $this->content = $content ?? ''; + } + + /** + * Get value from a page variable (used mostly for creating edit forms). + * + * @param string $name Variable name. + * @param mixed $default + * @return mixed + */ + public function value($name, $default = null) + { + if ($name === 'content') { + return $this->raw_content; + } + if ($name === 'route') { + $parent = $this->parent(); + + return $parent ? $parent->rawRoute() : ''; + } + if ($name === 'order') { + $order = $this->order(); + + return $order ? (int)$this->order() : ''; + } + if ($name === 'ordering') { + return (bool)$this->order(); + } + if ($name === 'folder') { + return preg_replace(PAGE_ORDER_PREFIX_REGEX, '', $this->folder); + } + if ($name === 'slug') { + return $this->slug(); + } + if ($name === 'name') { + $name = $this->name(); + $language = $this->language() ? '.' . $this->language() : ''; + $pattern = '%(' . preg_quote($language, '%') . ')?\.md$%'; + $name = preg_replace($pattern, '', $name); + + if ($this->isModule()) { + return 'modular/' . $name; + } + + return $name; + } + if ($name === 'media') { + return $this->media()->all(); + } + if ($name === 'media.file') { + return $this->media()->files(); + } + if ($name === 'media.video') { + return $this->media()->videos(); + } + if ($name === 'media.image') { + return $this->media()->images(); + } + if ($name === 'media.audio') { + return $this->media()->audios(); + } + + $path = explode('.', $name); + $scope = array_shift($path); + + if ($name === 'frontmatter') { + return $this->frontmatter; + } + + if ($scope === 'header') { + $current = $this->header(); + foreach ($path as $field) { + if (is_object($current) && isset($current->{$field})) { + $current = $current->{$field}; + } elseif (is_array($current) && isset($current[$field])) { + $current = $current[$field]; + } else { + return $default; + } + } + + return $current; + } + + return $default; + } + + /** + * Gets and Sets the Page raw content + * + * @param string|null $var + * @return string + */ + public function rawMarkdown($var = null) + { + if ($var !== null) { + $this->raw_content = $var; + } + + return $this->raw_content; + } + + /** + * @return bool + * @internal + */ + public function translated(): bool + { + return $this->initialized; + } + + /** + * Get file object to the page. + * + * @return MarkdownFile|null + */ + public function file() + { + if ($this->name) { + return MarkdownFile::instance($this->filePath()); + } + + return null; + } + + /** + * Save page if there's a file assigned to it. + * + * @param bool|array $reorder Internal use. + */ + public function save($reorder = true) + { + // Perform move, copy [or reordering] if needed. + $this->doRelocation(); + + $file = $this->file(); + if ($file) { + $file->filename($this->filePath()); + $file->header((array)$this->header()); + $file->markdown($this->raw_content); + $file->save(); + } + + // Perform reorder if required + if ($reorder && is_array($reorder)) { + $this->doReorder($reorder); + } + + // We need to signal Flex Pages about the change. + /** @var Flex|null $flex */ + $flex = Grav::instance()['flex'] ?? null; + $directory = $flex ? $flex->getDirectory('pages') : null; + if (null !== $directory) { + $directory->clearCache(); + } + + $this->_original = null; + } + + /** + * Prepare move page to new location. Moves also everything that's under the current page. + * + * You need to call $this->save() in order to perform the move. + * + * @param PageInterface $parent New parent page. + * @return $this + */ + public function move(PageInterface $parent) + { + if (!$this->_original) { + $clone = clone $this; + $this->_original = $clone; + } + + $this->_action = 'move'; + + if ($this->route() === $parent->route()) { + throw new RuntimeException('Failed: Cannot set page parent to self'); + } + if (Utils::startsWith($parent->rawRoute(), $this->rawRoute())) { + throw new RuntimeException('Failed: Cannot set page parent to a child of current page'); + } + + $this->parent($parent); + $this->id(time() . md5($this->filePath())); + + if ($parent->path()) { + $this->path($parent->path() . '/' . $this->folder()); + } + + if ($parent->route()) { + $this->route($parent->route() . '/' . $this->slug()); + } else { + $this->route(Grav::instance()['pages']->root()->route() . '/' . $this->slug()); + } + + $this->raw_route = null; + + return $this; + } + + /** + * Prepare a copy from the page. Copies also everything that's under the current page. + * + * Returns a new Page object for the copy. + * You need to call $this->save() in order to perform the move. + * + * @param PageInterface $parent New parent page. + * @return $this + */ + public function copy(PageInterface $parent) + { + $this->move($parent); + $this->_action = 'copy'; + + return $this; + } + + /** + * Get blueprints for the page. + * + * @return Blueprint + */ + public function blueprints() + { + $grav = Grav::instance(); + + /** @var Pages $pages */ + $pages = $grav['pages']; + + $blueprint = $pages->blueprints($this->blueprintName()); + $fields = $blueprint->fields(); + $edit_mode = isset($grav['admin']) ? $grav['config']->get('plugins.admin.edit_mode') : null; + + // override if you only want 'normal' mode + if (empty($fields) && ($edit_mode === 'auto' || $edit_mode === 'normal')) { + $blueprint = $pages->blueprints('default'); + } + + // override if you only want 'expert' mode + if (!empty($fields) && $edit_mode === 'expert') { + $blueprint = $pages->blueprints(''); + } + + return $blueprint; + } + + /** + * Get the blueprint name for this page. Use the blueprint form field if set + * + * @return string + */ + public function blueprintName() + { + $blueprint_name = filter_input(INPUT_POST, 'blueprint', FILTER_SANITIZE_STRING) ?: $this->template(); + + return $blueprint_name; + } + + /** + * Validate page header. + * + * @return void + * @throws Exception + */ + public function validate() + { + $blueprints = $this->blueprints(); + $blueprints->validate($this->toArray()); + } + + /** + * Filter page header from illegal contents. + * + * @return void + */ + public function filter() + { + $blueprints = $this->blueprints(); + $values = $blueprints->filter($this->toArray()); + if ($values && isset($values['header'])) { + $this->header($values['header']); + } + } + + /** + * Get unknown header variables. + * + * @return array + */ + public function extra() + { + $blueprints = $this->blueprints(); + + return $blueprints->extra($this->toArray()['header'], 'header.'); + } + + /** + * Convert page to an array. + * + * @return array + */ + public function toArray() + { + return [ + 'header' => (array)$this->header(), + 'content' => (string)$this->value('content') + ]; + } + + /** + * Convert page to YAML encoded string. + * + * @return string + */ + public function toYaml() + { + return Yaml::dump($this->toArray(), 20); + } + + /** + * Convert page to JSON encoded string. + * + * @return string + */ + public function toJson() + { + return json_encode($this->toArray()); + } + + /** + * @return string + */ + public function getCacheKey(): string + { + return $this->id(); + } + + /** + * Gets and sets the associated media as found in the page folder. + * + * @param Media|null $var Representation of associated media. + * @return Media Representation of associated media. + */ + public function media($var = null) + { + if ($var) { + $this->setMedia($var); + } + + /** @var Media $media */ + $media = $this->getMedia(); + + return $media; + } + + /** + * Get filesystem path to the associated media. + * + * @return string|null + */ + public function getMediaFolder() + { + return $this->path(); + } + + /** + * Get display order for the associated media. + * + * @return array Empty array means default ordering. + */ + public function getMediaOrder() + { + $header = $this->header(); + + return isset($header->media_order) ? array_map('trim', explode(',', (string)$header->media_order)) : []; + } + + /** + * Gets and sets the name field. If no name field is set, it will return 'default.md'. + * + * @param string|null $var The name of this page. + * @return string The name of this page. + */ + public function name($var = null) + { + if ($var !== null) { + $this->name = $var; + } + + return $this->name ?: 'default.md'; + } + + /** + * Returns child page type. + * + * @return string + */ + public function childType() + { + return isset($this->header->child_type) ? (string)$this->header->child_type : ''; + } + + /** + * Gets and sets the template field. This is used to find the correct Twig template file to render. + * If no field is set, it will return the name without the .md extension + * + * @param string|null $var the template name + * @return string the template name + */ + public function template($var = null) + { + if ($var !== null) { + $this->template = $var; + } + if (empty($this->template)) { + $this->template = ($this->isModule() ? 'modular/' : '') . str_replace($this->extension(), '', $this->name()); + } + + return $this->template; + } + + /** + * Allows a page to override the output render format, usually the extension provided in the URL. + * (e.g. `html`, `json`, `xml`, etc). + * + * @param string|null $var + * @return string + */ + public function templateFormat($var = null) + { + if (null !== $var) { + $this->template_format = is_string($var) ? $var : null; + } + + if (!isset($this->template_format)) { + $this->template_format = ltrim($this->header->append_url_extension ?? Utils::getPageFormat(), '.'); + } + + return $this->template_format; + } + + /** + * Gets and sets the extension field. + * + * @param string|null $var + * @return string + */ + public function extension($var = null) + { + if ($var !== null) { + $this->extension = $var; + } + if (empty($this->extension)) { + $this->extension = '.' . pathinfo($this->name(), PATHINFO_EXTENSION); + } + + return $this->extension; + } + + /** + * Returns the page extension, got from the page `url_extension` config and falls back to the + * system config `system.pages.append_url_extension`. + * + * @return string The extension of this page. For example `.html` + */ + public function urlExtension() + { + if ($this->home()) { + return ''; + } + + // if not set in the page get the value from system config + if (null === $this->url_extension) { + $this->url_extension = Grav::instance()['config']->get('system.pages.append_url_extension', ''); + } + + return $this->url_extension; + } + + /** + * Gets and sets the expires field. If not set will return the default + * + * @param int|null $var The new expires value. + * @return int The expires value + */ + public function expires($var = null) + { + if ($var !== null) { + $this->expires = $var; + } + + return $this->expires ?? Grav::instance()['config']->get('system.pages.expires'); + } + + /** + * Gets and sets the cache-control property. If not set it will return the default value (null) + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details on valid options + * + * @param string|null $var + * @return string|null + */ + public function cacheControl($var = null) + { + if ($var !== null) { + $this->cache_control = $var; + } + + return $this->cache_control ?? Grav::instance()['config']->get('system.pages.cache_control'); + } + + /** + * Gets and sets the title for this Page. If no title is set, it will use the slug() to get a name + * + * @param string|null $var the title of the Page + * @return string the title of the Page + */ + public function title($var = null) + { + if ($var !== null) { + $this->title = $var; + } + if (empty($this->title)) { + $this->title = ucfirst($this->slug()); + } + + return $this->title; + } + + /** + * Gets and sets the menu name for this Page. This is the text that can be used specifically for navigation. + * If no menu field is set, it will use the title() + * + * @param string|null $var the menu field for the page + * @return string the menu field for the page + */ + public function menu($var = null) + { + if ($var !== null) { + $this->menu = $var; + } + if (empty($this->menu)) { + $this->menu = $this->title(); + } + + return $this->menu; + } + + /** + * Gets and Sets whether or not this Page is visible for navigation + * + * @param bool|null $var true if the page is visible + * @return bool true if the page is visible + */ + public function visible($var = null) + { + if ($var !== null) { + $this->visible = (bool)$var; + } + + if ($this->visible === null) { + // Set item visibility in menu if folder is different from slug + // eg folder = 01.Home and slug = Home + if (preg_match(PAGE_ORDER_PREFIX_REGEX, $this->folder)) { + $this->visible = true; + } else { + $this->visible = false; + } + } + + return $this->visible; + } + + /** + * Gets and Sets whether or not this Page is considered published + * + * @param bool|null $var true if the page is published + * @return bool true if the page is published + */ + public function published($var = null) + { + if ($var !== null) { + $this->published = (bool)$var; + } + + // If not published, should not be visible in menus either + if ($this->published === false) { + $this->visible = false; + } + + return $this->published; + } + + /** + * Gets and Sets the Page publish date + * + * @param string|null $var string representation of a date + * @return int unix timestamp representation of the date + */ + public function publishDate($var = null) + { + if ($var !== null) { + $this->publish_date = Utils::date2timestamp($var, $this->dateformat); + } + + return $this->publish_date; + } + + /** + * Gets and Sets the Page unpublish date + * + * @param string|null $var string representation of a date + * @return int|null unix timestamp representation of the date + */ + public function unpublishDate($var = null) + { + if ($var !== null) { + $this->unpublish_date = Utils::date2timestamp($var, $this->dateformat); + } + + return $this->unpublish_date; + } + + /** + * Gets and Sets whether or not this Page is routable, ie you can reach it + * via a URL. + * The page must be *routable* and *published* + * + * @param bool|null $var true if the page is routable + * @return bool true if the page is routable + */ + public function routable($var = null) + { + if ($var !== null) { + $this->routable = (bool)$var; + } + + return $this->routable && $this->published(); + } + + /** + * @param bool|null $var + * @return bool + */ + public function ssl($var = null) + { + if ($var !== null) { + $this->ssl = (bool)$var; + } + + return $this->ssl; + } + + /** + * Gets and Sets the process setup for this Page. This is multi-dimensional array that consists of + * a simple array of arrays with the form array("markdown"=>true) for example + * + * @param array|null $var an Array of name value pairs where the name is the process and value is true or false + * @return array an Array of name value pairs where the name is the process and value is true or false + */ + public function process($var = null) + { + if ($var !== null) { + $this->process = (array)$var; + } + + return $this->process; + } + + /** + * Returns the state of the debugger override setting for this page + * + * @return bool + */ + public function debugger() + { + return !(isset($this->debugger) && $this->debugger === false); + } + + /** + * Function to merge page metadata tags and build an array of Metadata objects + * that can then be rendered in the page. + * + * @param array|null $var an Array of metadata values to set + * @return array an Array of metadata values for the page + */ + public function metadata($var = null) + { + if ($var !== null) { + $this->metadata = (array)$var; + } + + // if not metadata yet, process it. + if (null === $this->metadata) { + $header_tag_http_equivs = ['content-type', 'default-style', 'refresh', 'x-ua-compatible', 'content-security-policy']; + + $this->metadata = []; + + // Set the Generator tag + $metadata = [ + 'generator' => 'GravCMS' + ]; + + $config = Grav::instance()['config']; + + $escape = !$config->get('system.strict_mode.twig_compat', false) || $config->get('system.twig.autoescape', true); + + // Get initial metadata for the page + $metadata = array_merge($metadata, $config->get('site.metadata', [])); + + if (isset($this->header->metadata) && is_array($this->header->metadata)) { + // Merge any site.metadata settings in with page metadata + $metadata = array_merge($metadata, $this->header->metadata); + } + + // Build an array of meta objects.. + foreach ((array)$metadata as $key => $value) { + // Lowercase the key + $key = strtolower($key); + // If this is a property type metadata: "og", "twitter", "facebook" etc + // Backward compatibility for nested arrays in metas + if (is_array($value)) { + foreach ($value as $property => $prop_value) { + $prop_key = $key . ':' . $property; + $this->metadata[$prop_key] = [ + 'name' => $prop_key, + 'property' => $prop_key, + 'content' => $escape ? htmlspecialchars($prop_value, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $prop_value + ]; + } + } else { + // If it this is a standard meta data type + if ($value) { + if (in_array($key, $header_tag_http_equivs, true)) { + $this->metadata[$key] = [ + 'http_equiv' => $key, + 'content' => $escape ? htmlspecialchars($value, ENT_COMPAT, 'UTF-8') : $value + ]; + } elseif ($key === 'charset') { + $this->metadata[$key] = ['charset' => $escape ? htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $value]; + } else { + // if it's a social metadata with separator, render as property + $separator = strpos($key, ':'); + $hasSeparator = $separator && $separator < strlen($key) - 1; + $entry = [ + 'content' => $escape ? htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $value + ]; + + if ($hasSeparator && !Utils::startsWith($key, ['twitter', 'flattr'])) { + $entry['property'] = $key; + } else { + $entry['name'] = $key; + } + + $this->metadata[$key] = $entry; + } + } + } + } + } + + return $this->metadata; + } + + /** + * Reset the metadata and pull from header again + */ + public function resetMetadata() + { + $this->metadata = null; + } + + /** + * Gets and Sets the slug for the Page. The slug is used in the URL routing. If not set it uses + * the parent folder from the path + * + * @param string|null $var the slug, e.g. 'my-blog' + * @return string the slug + */ + public function slug($var = null) + { + if ($var !== null && $var !== '') { + $this->slug = $var; + } + + if (empty($this->slug)) { + $this->slug = $this->adjustRouteCase(preg_replace(PAGE_ORDER_PREFIX_REGEX, '', $this->folder)) ?: null; + } + + return $this->slug; + } + + /** + * Get/set order number of this page. + * + * @param int|null $var + * @return string|bool + */ + public function order($var = null) + { + if ($var !== null) { + $order = $var ? sprintf('%02d.', (int)$var) : ''; + $this->folder($order . preg_replace(PAGE_ORDER_PREFIX_REGEX, '', $this->folder)); + + return $order; + } + + preg_match(PAGE_ORDER_PREFIX_REGEX, $this->folder, $order); + + return $order[0] ?? false; + } + + /** + * Gets the URL for a page - alias of url(). + * + * @param bool $include_host + * @return string the permalink + */ + public function link($include_host = false) + { + return $this->url($include_host); + } + + /** + * Gets the URL with host information, aka Permalink. + * @return string The permalink. + */ + public function permalink() + { + return $this->url(true, false, true, true); + } + + /** + * Returns the canonical URL for a page + * + * @param bool $include_lang + * @return string + */ + public function canonical($include_lang = true) + { + return $this->url(true, true, $include_lang); + } + + /** + * Gets the url for the Page. + * + * @param bool $include_host Defaults false, but true would include http://yourhost.com + * @param bool $canonical True to return the canonical URL + * @param bool $include_base Include base url on multisite as well as language code + * @param bool $raw_route + * @return string The url. + */ + public function url($include_host = false, $canonical = false, $include_base = true, $raw_route = false) + { + // Override any URL when external_url is set + if (isset($this->external_url)) { + return $this->external_url; + } + + $grav = Grav::instance(); + + /** @var Pages $pages */ + $pages = $grav['pages']; + + /** @var Config $config */ + $config = $grav['config']; + + // get base route (multi-site base and language) + $route = $include_base ? $pages->baseRoute() : ''; + + // add full route if configured to do so + if (!$include_host && $config->get('system.absolute_urls', false)) { + $include_host = true; + } + + if ($canonical) { + $route .= $this->routeCanonical(); + } elseif ($raw_route) { + $route .= $this->rawRoute(); + } else { + $route .= $this->route(); + } + + /** @var Uri $uri */ + $uri = $grav['uri']; + $url = $uri->rootUrl($include_host) . '/' . trim($route, '/') . $this->urlExtension(); + + return Uri::filterPath($url); + } + + /** + * Gets the route for the page based on the route headers if available, else from + * the parents route and the current Page's slug. + * + * @param string|null $var Set new default route. + * @return string|null The route for the Page. + */ + public function route($var = null) + { + if ($var !== null) { + $this->route = $var; + } + + if (empty($this->route)) { + $baseRoute = null; + + // calculate route based on parent slugs + $parent = $this->parent(); + if (isset($parent)) { + if ($this->hide_home_route && $parent->route() === $this->home_route) { + $baseRoute = ''; + } else { + $baseRoute = (string)$parent->route(); + } + } + + $this->route = isset($baseRoute) ? $baseRoute . '/' . $this->slug() : null; + + if (!empty($this->routes) && isset($this->routes['default'])) { + $this->routes['aliases'][] = $this->route; + $this->route = $this->routes['default']; + + return $this->route; + } + } + + return $this->route; + } + + /** + * Helper method to clear the route out so it regenerates next time you use it + */ + public function unsetRouteSlug() + { + unset($this->route, $this->slug); + } + + /** + * Gets and Sets the page raw route + * + * @param string|null $var + * @return null|string + */ + public function rawRoute($var = null) + { + if ($var !== null) { + $this->raw_route = $var; + } + + if (empty($this->raw_route)) { + $parent = $this->parent(); + $baseRoute = $parent ? (string)$parent->rawRoute() : null; + + $slug = $this->adjustRouteCase(preg_replace(PAGE_ORDER_PREFIX_REGEX, '', $this->folder)); + + $this->raw_route = isset($baseRoute) ? $baseRoute . '/' . $slug : null; + } + + return $this->raw_route; + } + + /** + * Gets the route aliases for the page based on page headers. + * + * @param array|null $var list of route aliases + * @return array The route aliases for the Page. + */ + public function routeAliases($var = null) + { + if ($var !== null) { + $this->routes['aliases'] = (array)$var; + } + + if (!empty($this->routes) && isset($this->routes['aliases'])) { + return $this->routes['aliases']; + } + + return []; + } + + /** + * Gets the canonical route for this page if its set. If provided it will use + * that value, else if it's `true` it will use the default route. + * + * @param string|null $var + * @return bool|string + */ + public function routeCanonical($var = null) + { + if ($var !== null) { + $this->routes['canonical'] = $var; + } + + if (!empty($this->routes) && isset($this->routes['canonical'])) { + return $this->routes['canonical']; + } + + return $this->route(); + } + + /** + * Gets and sets the identifier for this Page object. + * + * @param string|null $var the identifier + * @return string the identifier + */ + public function id($var = null) + { + if (null === $this->id) { + // We need to set unique id to avoid potential cache conflicts between pages. + $var = time() . md5($this->filePath()); + } + if ($var !== null) { + // store unique per language + $active_lang = Grav::instance()['language']->getLanguage() ?: ''; + $id = $active_lang . $var; + $this->id = $id; + } + + return $this->id; + } + + /** + * Gets and sets the modified timestamp. + * + * @param int|null $var modified unix timestamp + * @return int modified unix timestamp + */ + public function modified($var = null) + { + if ($var !== null) { + $this->modified = $var; + } + + return $this->modified; + } + + /** + * Gets the redirect set in the header. + * + * @param string|null $var redirect url + * @return string|null + */ + public function redirect($var = null) + { + if ($var !== null) { + $this->redirect = $var; + } + + return $this->redirect ?: null; + } + + /** + * Gets and sets the option to show the etag header for the page. + * + * @param bool|null $var show etag header + * @return bool show etag header + */ + public function eTag($var = null): bool + { + if ($var !== null) { + $this->etag = $var; + } + if (!isset($this->etag)) { + $this->etag = (bool)Grav::instance()['config']->get('system.pages.etag'); + } + + return $this->etag ?? false; + } + + /** + * Gets and sets the option to show the last_modified header for the page. + * + * @param bool|null $var show last_modified header + * @return bool show last_modified header + */ + public function lastModified($var = null) + { + if ($var !== null) { + $this->last_modified = $var; + } + if (!isset($this->last_modified)) { + $this->last_modified = (bool)Grav::instance()['config']->get('system.pages.last_modified'); + } + + return $this->last_modified; + } + + /** + * Gets and sets the path to the .md file for this Page object. + * + * @param string|null $var the file path + * @return string|null the file path + */ + public function filePath($var = null) + { + if ($var !== null) { + // Filename of the page. + $this->name = basename($var); + // Folder of the page. + $this->folder = basename(dirname($var)); + // Path to the page. + $this->path = dirname($var, 2); + } + + return rtrim($this->path . '/' . $this->folder . '/' . ($this->name() ?: ''), '/'); + } + + /** + * Gets the relative path to the .md file + * + * @return string The relative file path + */ + public function filePathClean() + { + return str_replace(GRAV_ROOT . DS, '', $this->filePath()); + } + + /** + * Returns the clean path to the page file + * + * @return string + */ + public function relativePagePath() + { + return str_replace('/' . $this->name(), '', $this->filePathClean()); + } + + /** + * Gets and sets the path to the folder where the .md for this Page object resides. + * This is equivalent to the filePath but without the filename. + * + * @param string|null $var the path + * @return string|null the path + */ + public function path($var = null) + { + if ($var !== null) { + // Folder of the page. + $this->folder = basename($var); + // Path to the page. + $this->path = dirname($var); + } + + return $this->path ? $this->path . '/' . $this->folder : null; + } + + /** + * Get/set the folder. + * + * @param string|null $var Optional path + * @return string|null + */ + public function folder($var = null) + { + if ($var !== null) { + $this->folder = $var; + } + + return $this->folder; + } + + /** + * Gets and sets the date for this Page object. This is typically passed in via the page headers + * + * @param string|null $var string representation of a date + * @return int unix timestamp representation of the date + */ + public function date($var = null) + { + if ($var !== null) { + $this->date = Utils::date2timestamp($var, $this->dateformat); + } + + if (!$this->date) { + $this->date = $this->modified; + } + + return $this->date; + } + + /** + * Gets and sets the date format for this Page object. This is typically passed in via the page headers + * using typical PHP date string structure - http://php.net/manual/en/function.date.php + * + * @param string|null $var string representation of a date format + * @return string string representation of a date format + */ + public function dateformat($var = null) + { + if ($var !== null) { + $this->dateformat = $var; + } + + return $this->dateformat; + } + + /** + * Gets and sets the order by which any sub-pages should be sorted. + * + * @param string|null $var the order, either "asc" or "desc" + * @return string the order, either "asc" or "desc" + * @deprecated 1.6 + */ + public function orderDir($var = null) + { + //user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED); + + if ($var !== null) { + $this->order_dir = $var; + } + + if (empty($this->order_dir)) { + $this->order_dir = 'asc'; + } + + return $this->order_dir; + } + + /** + * Gets and sets the order by which the sub-pages should be sorted. + * + * default - is the order based on the file system, ie 01.Home before 02.Advark + * title - is the order based on the title set in the pages + * date - is the order based on the date set in the pages + * folder - is the order based on the name of the folder with any numerics omitted + * + * @param string|null $var supported options include "default", "title", "date", and "folder" + * @return string supported options include "default", "title", "date", and "folder" + * @deprecated 1.6 + */ + public function orderBy($var = null) + { + //user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED); + + if ($var !== null) { + $this->order_by = $var; + } + + return $this->order_by; + } + + /** + * Gets the manual order set in the header. + * + * @param string|null $var supported options include "default", "title", "date", and "folder" + * @return array + * @deprecated 1.6 + */ + public function orderManual($var = null) + { + //user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED); + + if ($var !== null) { + $this->order_manual = $var; + } + + return (array)$this->order_manual; + } + + /** + * Gets and sets the maxCount field which describes how many sub-pages should be displayed if the + * sub_pages header property is set for this page object. + * + * @param int|null $var the maximum number of sub-pages + * @return int the maximum number of sub-pages + * @deprecated 1.6 + */ + public function maxCount($var = null) + { + //user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED); + + if ($var !== null) { + $this->max_count = (int)$var; + } + if (empty($this->max_count)) { + /** @var Config $config */ + $config = Grav::instance()['config']; + $this->max_count = (int)$config->get('system.pages.list.count'); + } + + return $this->max_count; + } + + /** + * Gets and sets the taxonomy array which defines which taxonomies this page identifies itself with. + * + * @param array|null $var an array of taxonomies + * @return array an array of taxonomies + */ + public function taxonomy($var = null) + { + if ($var !== null) { + // make sure first level are arrays + array_walk($var, static function (&$value) { + $value = (array) $value; + }); + // make sure all values are strings + array_walk_recursive($var, static function (&$value) { + $value = (string) $value; + }); + $this->taxonomy = $var; + } + + return $this->taxonomy; + } + + /** + * Gets and sets the modular var that helps identify this page is a modular child + * + * @param bool|null $var true if modular_twig + * @return bool true if modular_twig + * @deprecated 1.7 Use ->isModule() or ->modularTwig() method instead. + */ + public function modular($var = null) + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, use ->isModule() or ->modularTwig() method instead', E_USER_DEPRECATED); + + return $this->modularTwig($var); + } + + /** + * Gets and sets the modular_twig var that helps identify this page as a modular child page that will need + * twig processing handled differently from a regular page. + * + * @param bool|null $var true if modular_twig + * @return bool true if modular_twig + */ + public function modularTwig($var = null) + { + if ($var !== null) { + $this->modular_twig = (bool)$var; + if ($var) { + $this->visible(false); + // some routable logic + if (empty($this->header->routable)) { + $this->routable = false; + } + } + } + + return $this->modular_twig ?? false; + } + + /** + * Gets the configured state of the processing method. + * + * @param string $process the process, eg "twig" or "markdown" + * @return bool whether or not the processing method is enabled for this Page + */ + public function shouldProcess($process) + { + return (bool)($this->process[$process] ?? false); + } + + /** + * Gets and Sets the parent object for this page + * + * @param PageInterface|null $var the parent page object + * @return PageInterface|null the parent page object if it exists. + */ + public function parent(PageInterface $var = null) + { + if ($var) { + $this->parent = $var->path(); + + return $var; + } + + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->get($this->parent); + } + + /** + * Gets the top parent object for this page. Can return page itself. + * + * @return PageInterface The top parent page object. + */ + public function topParent() + { + $topParent = $this; + + while (true) { + $theParent = $topParent->parent(); + if ($theParent !== null && $theParent->parent() !== null) { + $topParent = $theParent; + } else { + break; + } + } + + return $topParent; + } + + /** + * Returns children of this page. + * + * @return PageCollectionInterface|Collection + */ + public function children() + { + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->children($this->path()); + } + + + /** + * Check to see if this item is the first in an array of sub-pages. + * + * @return bool True if item is first. + */ + public function isFirst() + { + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; + if ($collection instanceof Collection) { + return $collection->isFirst($this->path()); + } + + return true; + } + + /** + * Check to see if this item is the last in an array of sub-pages. + * + * @return bool True if item is last + */ + public function isLast() + { + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; + if ($collection instanceof Collection) { + return $collection->isLast($this->path()); + } + + return true; + } + + /** + * Gets the previous sibling based on current position. + * + * @return PageInterface the previous Page item + */ + public function prevSibling() + { + return $this->adjacentSibling(-1); + } + + /** + * Gets the next sibling based on current position. + * + * @return PageInterface the next Page item + */ + public function nextSibling() + { + return $this->adjacentSibling(1); + } + + /** + * Returns the adjacent sibling based on a direction. + * + * @param int $direction either -1 or +1 + * @return PageInterface|false the sibling page + */ + public function adjacentSibling($direction = 1) + { + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; + if ($collection instanceof Collection) { + return $collection->adjacentSibling($this->path(), $direction); + } + + return false; + } + + /** + * Returns the item in the current position. + * + * @return int|null The index of the current page. + */ + public function currentPosition() + { + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; + if ($collection instanceof Collection) { + return $collection->currentPosition($this->path()); + } + + return 1; + } + + /** + * Returns whether or not this page is the currently active page requested via the URL. + * + * @return bool True if it is active + */ + public function active() + { + $uri_path = rtrim(urldecode(Grav::instance()['uri']->path()), '/') ?: '/'; + $routes = Grav::instance()['pages']->routes(); + + return isset($routes[$uri_path]) && $routes[$uri_path] === $this->path(); + } + + /** + * Returns whether or not this URI's URL contains the URL of the active page. + * Or in other words, is this page's URL in the current URL + * + * @return bool True if active child exists + */ + public function activeChild() + { + $grav = Grav::instance(); + /** @var Uri $uri */ + $uri = $grav['uri']; + /** @var Pages $pages */ + $pages = $grav['pages']; + $uri_path = rtrim(urldecode($uri->path()), '/'); + $routes = $pages->routes(); + + if (isset($routes[$uri_path])) { + $page = $pages->find($uri->route()); + /** @var PageInterface|null $child_page */ + $child_page = $page ? $page->parent() : null; + while ($child_page && !$child_page->root()) { + if ($this->path() === $child_page->path()) { + return true; + } + $child_page = $child_page->parent(); + } + } + + return false; + } + + /** + * Returns whether or not this page is the currently configured home page. + * + * @return bool True if it is the homepage + */ + public function home() + { + $home = Grav::instance()['config']->get('system.home.alias'); + + return $this->route() === $home || $this->rawRoute() === $home; + } + + /** + * Returns whether or not this page is the root node of the pages tree. + * + * @return bool True if it is the root + */ + public function root() + { + return !$this->parent && !$this->name && !$this->visible; + } + + /** + * Helper method to return an ancestor page. + * + * @param bool|null $lookup Name of the parent folder + * @return PageInterface page you were looking for if it exists + */ + public function ancestor($lookup = null) + { + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->ancestor($this->route, $lookup); + } + + /** + * Helper method to return an ancestor page to inherit from. The current + * page object is returned. + * + * @param string $field Name of the parent folder + * @return PageInterface + */ + public function inherited($field) + { + [$inherited, $currentParams] = $this->getInheritedParams($field); + + $this->modifyHeader($field, $currentParams); + + return $inherited; + } + + /** + * Helper method to return an ancestor field only to inherit from. The + * first occurrence of an ancestor field will be returned if at all. + * + * @param string $field Name of the parent folder + * + * @return array + */ + public function inheritedField($field) + { + [$inherited, $currentParams] = $this->getInheritedParams($field); + + return $currentParams; + } + + /** + * Method that contains shared logic for inherited() and inheritedField() + * + * @param string $field Name of the parent folder + * @return array + */ + protected function getInheritedParams($field) + { + $pages = Grav::instance()['pages']; + + /** @var Pages $pages */ + $inherited = $pages->inherited($this->route, $field); + $inheritedParams = $inherited ? (array)$inherited->value('header.' . $field) : []; + $currentParams = (array)$this->value('header.' . $field); + if ($inheritedParams && is_array($inheritedParams)) { + $currentParams = array_replace_recursive($inheritedParams, $currentParams); + } + + return [$inherited, $currentParams]; + } + + /** + * Helper method to return a page. + * + * @param string $url the url of the page + * @param bool $all + * + * @return PageInterface page you were looking for if it exists + */ + public function find($url, $all = false) + { + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->find($url, $all); + } + + /** + * Get a collection of pages in the current context. + * + * @param string|array $params + * @param bool $pagination + * + * @return PageCollectionInterface|Collection + * @throws InvalidArgumentException + */ + public function collection($params = 'content', $pagination = true) + { + if (is_string($params)) { + // Look into a page header field. + $params = (array)$this->value('header.' . $params); + } elseif (!is_array($params)) { + throw new InvalidArgumentException('Argument should be either header variable name or array of parameters'); + } + + $params['filter'] = ($params['filter'] ?? []) + ['translated' => true]; + $context = [ + 'pagination' => $pagination, + 'self' => $this + ]; + + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->getCollection($params, $context); + } + + /** + * @param string|array $value + * @param bool $only_published + * @return PageCollectionInterface|Collection + */ + public function evaluate($value, $only_published = true) + { + $params = [ + 'items' => $value, + 'published' => $only_published + ]; + $context = [ + 'event' => false, + 'pagination' => false, + 'url_taxonomy_filters' => false, + 'self' => $this + ]; + + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->getCollection($params, $context); + } + + /** + * Returns whether or not this Page object has a .md file associated with it or if its just a directory. + * + * @return bool True if its a page with a .md file associated + */ + public function isPage() + { + if ($this->name) { + return true; + } + + return false; + } + + /** + * Returns whether or not this Page object is a directory or a page. + * + * @return bool True if its a directory + */ + public function isDir() + { + return !$this->isPage(); + } + + /** + * @return bool + */ + public function isModule(): bool + { + return $this->modularTwig(); + } + + /** + * Returns whether the page exists in the filesystem. + * + * @return bool + */ + public function exists() + { + $file = $this->file(); + + return $file && $file->exists(); + } + + /** + * Returns whether or not the current folder exists + * + * @return bool + */ + public function folderExists() + { + return file_exists($this->path()); + } + + /** + * Cleans the path. + * + * @param string $path the path + * @return string the path + */ + protected function cleanPath($path) + { + $lastchunk = strrchr($path, DS); + if (strpos($lastchunk, ':') !== false) { + $path = str_replace($lastchunk, '', $path); + } + + return $path; + } + + /** + * Reorders all siblings according to a defined order + * + * @param array|null $new_order + */ + protected function doReorder($new_order) + { + if (!$this->_original) { + return; + } + + $pages = Grav::instance()['pages']; + $pages->init(); + + $this->_original->path($this->path()); + + $parent = $this->parent(); + $siblings = $parent ? $parent->children() : null; + + if ($siblings) { + $siblings->order('slug', 'asc', $new_order); + + $counter = 0; + + // Reorder all moved pages. + foreach ($siblings as $slug => $page) { + $order = (int)trim($page->order(), '.'); + $counter++; + + if ($order) { + if ($page->path() === $this->path() && $this->folderExists()) { + // Handle current page; we do want to change ordering number, but nothing else. + $this->order($counter); + $this->save(false); + } else { + // Handle all the other pages. + $page = $pages->get($page->path()); + if ($page && $page->folderExists() && !$page->_action) { + $page = $page->move($this->parent()); + $page->order($counter); + $page->save(false); + } + } + } + } + } + } + + /** + * Moves or copies the page in filesystem. + * + * @internal + * @return void + * @throws Exception + */ + protected function doRelocation() + { + if (!$this->_original) { + return; + } + + if (is_dir($this->_original->path())) { + if ($this->_action === 'move') { + Folder::move($this->_original->path(), $this->path()); + } elseif ($this->_action === 'copy') { + Folder::copy($this->_original->path(), $this->path()); + } + } + + if ($this->name() !== $this->_original->name()) { + $path = $this->path(); + if (is_file($path . '/' . $this->_original->name())) { + rename($path . '/' . $this->_original->name(), $path . '/' . $this->name()); + } + } + } + + /** + * @return void + */ + protected function setPublishState() + { + // Handle publishing dates if no explicit published option set + if (Grav::instance()['config']->get('system.pages.publish_dates') && !isset($this->header->published)) { + // unpublish if required, if not clear cache right before page should be unpublished + if ($this->unpublishDate()) { + if ($this->unpublishDate() < time()) { + $this->published(false); + } else { + $this->published(); + Grav::instance()['cache']->setLifeTime($this->unpublishDate()); + } + } + // publish if required, if not clear cache right before page is published + if ($this->publishDate() && $this->publishDate() > time()) { + $this->published(false); + Grav::instance()['cache']->setLifeTime($this->publishDate()); + } + } + } + + /** + * @param string $route + * @return string + */ + protected function adjustRouteCase($route) + { + $case_insensitive = Grav::instance()['config']->get('system.force_lowercase_urls'); + + return $case_insensitive ? mb_strtolower($route) : $route; + } + + /** + * Gets the Page Unmodified (original) version of the page. + * + * @return PageInterface The original version of the page. + */ + public function getOriginal() + { + return $this->_original; + } + + /** + * Gets the action. + * + * @return string|null The Action string. + */ + public function getAction() + { + return $this->_action; + } +} diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php new file mode 100644 index 0000000..9e5d47d --- /dev/null +++ b/system/src/Grav/Common/Page/Pages.php @@ -0,0 +1,2202 @@ + */ + protected $instances = []; + /** @var array */ + protected $index = []; + /** @var array */ + protected $children; + /** @var string */ + protected $base = ''; + /** @var string[] */ + protected $baseRoute = []; + /** @var string[] */ + protected $routes = []; + /** @var array */ + protected $sort; + /** @var Blueprints */ + protected $blueprints; + /** @var bool */ + protected $enable_pages = true; + /** @var int */ + protected $last_modified; + /** @var string[] */ + protected $ignore_files; + /** @var string[] */ + protected $ignore_folders; + /** @var bool */ + protected $ignore_hidden; + /** @var string */ + protected $check_method; + /** @var string */ + protected $pages_cache_id; + /** @var bool */ + protected $initialized = false; + /** @var string */ + protected $active_lang; + /** @var bool */ + protected $fire_events = false; + /** @var Types|null */ + protected static $types; + /** @var string|null */ + protected static $home_route; + + /** + * Constructor + * + * @param Grav $grav + */ + public function __construct(Grav $grav) + { + $this->grav = $grav; + } + + /** + * @return FlexDirectory|null + */ + public function getDirectory(): ?FlexDirectory + { + return $this->directory; + } + + /** + * Method used in admin to disable frontend pages from being initialized. + */ + public function disablePages(): void + { + $this->enable_pages = false; + } + + /** + * Method used in admin to later load frontend pages. + */ + public function enablePages(): void + { + if (!$this->enable_pages) { + $this->enable_pages = true; + + $this->init(); + } + } + + /** + * Get or set base path for the pages. + * + * @param string|null $path + * @return string + */ + public function base($path = null) + { + if ($path !== null) { + $path = trim($path, '/'); + $this->base = $path ? '/' . $path : ''; + $this->baseRoute = []; + } + + return $this->base; + } + + /** + * + * Get base route for Grav pages. + * + * @param string|null $lang Optional language code for multilingual routes. + * @return string + */ + public function baseRoute($lang = null) + { + $key = $lang ?: $this->active_lang ?: 'default'; + + if (!isset($this->baseRoute[$key])) { + /** @var Language $language */ + $language = $this->grav['language']; + + $path_base = rtrim($this->base(), '/'); + $path_lang = $language->enabled() ? $language->getLanguageURLPrefix($lang) : ''; + + $this->baseRoute[$key] = $path_base . $path_lang; + } + + return $this->baseRoute[$key]; + } + + /** + * + * Get route for Grav site. + * + * @param string $route Optional route to the page. + * @param string|null $lang Optional language code for multilingual links. + * @return string + */ + public function route($route = '/', $lang = null) + { + if (!$route || $route === '/') { + return $this->baseRoute($lang) ?: '/'; + } + + return $this->baseRoute($lang) . $route; + } + + /** + * Get relative referrer route and language code. Returns null if the route isn't within the current base, language (if set) and route. + * + * @example `$langCode = null; $referrer = $pages->referrerRoute($langCode, '/admin');` returns relative referrer url within /admin and updates $langCode + * @example `$langCode = 'en'; $referrer = $pages->referrerRoute($langCode, '/admin');` returns relative referrer url within the /en/admin + * + * @param string|null $langCode Variable to store the language code. If already set, check only against that language. + * @param string $route Optional route within the site. + * @return string|null + * @since 1.7.23 + */ + public function referrerRoute(?string &$langCode, string $route = '/'): ?string + { + $referrer = $_SERVER['HTTP_REFERER'] ?? null; + + // Start by checking that referrer came from our site. + $root = $this->grav['base_url_absolute']; + if (!is_string($referrer) || !str_starts_with($referrer, $root)) { + return null; + } + + /** @var Language $language */ + $language = $this->grav['language']; + + // Get all language codes and append no language. + if (null === $langCode) { + $languages = $language->enabled() ? $language->getLanguages() : []; + $languages[] = ''; + } else { + $languages[] = $langCode; + } + + $path_base = rtrim($this->base(), '/'); + $path_route = rtrim($route, '/'); + + // Try to figure out the language code. + foreach ($languages as $code) { + $path_lang = $code ? "/{$code}" : ''; + + $base = $path_base . $path_lang . $path_route; + if ($referrer === $base || str_starts_with($referrer, "{$base}/")) { + if (null === $langCode) { + $langCode = $code; + } + + return substr($referrer, \strlen($base)); + } + } + + return null; + } + + /** + * + * Get base URL for Grav pages. + * + * @param string|null $lang Optional language code for multilingual links. + * @param bool|null $absolute If true, return absolute url, if false, return relative url. Otherwise return default. + * @return string + */ + public function baseUrl($lang = null, $absolute = null) + { + if ($absolute === null) { + $type = 'base_url'; + } elseif ($absolute) { + $type = 'base_url_absolute'; + } else { + $type = 'base_url_relative'; + } + + return $this->grav[$type] . $this->baseRoute($lang); + } + + /** + * + * Get home URL for Grav site. + * + * @param string|null $lang Optional language code for multilingual links. + * @param bool|null $absolute If true, return absolute url, if false, return relative url. Otherwise return default. + * @return string + */ + public function homeUrl($lang = null, $absolute = null) + { + return $this->baseUrl($lang, $absolute) ?: '/'; + } + + /** + * + * Get URL for Grav site. + * + * @param string $route Optional route to the page. + * @param string|null $lang Optional language code for multilingual links. + * @param bool|null $absolute If true, return absolute url, if false, return relative url. Otherwise return default. + * @return string + */ + public function url($route = '/', $lang = null, $absolute = null) + { + if (!$route || $route === '/') { + return $this->homeUrl($lang, $absolute); + } + + return $this->baseUrl($lang, $absolute) . Uri::filterPath($route); + } + + /** + * @param string $method + * @return void + */ + public function setCheckMethod($method): void + { + $this->check_method = strtolower($method); + } + + /** + * @return void + */ + public function register(): void + { + $config = $this->grav['config']; + $type = $config->get('system.pages.type'); + if ($type === 'flex') { + $this->initFlexPages(); + } + } + + /** + * Reset pages (used in search indexing etc). + * + * @return void + */ + public function reset(): void + { + $this->initialized = false; + + $this->init(); + } + + /** + * Class initialization. Must be called before using this class. + */ + public function init(): void + { + if ($this->initialized) { + return; + } + + $config = $this->grav['config']; + $this->ignore_files = (array)$config->get('system.pages.ignore_files'); + $this->ignore_folders = (array)$config->get('system.pages.ignore_folders'); + $this->ignore_hidden = (bool)$config->get('system.pages.ignore_hidden'); + $this->fire_events = (bool)$config->get('system.pages.events.page'); + + $this->instances = []; + $this->index = []; + $this->children = []; + $this->routes = []; + + if (!$this->check_method) { + $this->setCheckMethod($config->get('system.cache.check.method', 'file')); + } + + if ($this->enable_pages === false) { + $page = $this->buildRootPage(); + $this->instances[$page->path()] = $page; + + return; + } + + $this->buildPages(); + + $this->initialized = true; + } + + /** + * Get or set last modification time. + * + * @param int|null $modified + * @return int|null + */ + public function lastModified($modified = null) + { + if ($modified && $modified > $this->last_modified) { + $this->last_modified = $modified; + } + + return $this->last_modified; + } + + /** + * Returns a list of all pages. + * + * @return PageInterface[] + */ + public function instances() + { + $instances = []; + foreach ($this->index as $path => $instance) { + $page = $this->get($path); + if ($page) { + $instances[$path] = $page; + } + } + + return $instances; + } + + /** + * Returns a list of all routes. + * + * @return array + */ + public function routes() + { + return $this->routes; + } + + /** + * Adds a page and assigns a route to it. + * + * @param PageInterface $page Page to be added. + * @param string|null $route Optional route (uses route from the object if not set). + */ + public function addPage(PageInterface $page, $route = null): void + { + $path = $page->path() ?? ''; + if (!isset($this->index[$path])) { + $this->index[$path] = $page; + $this->instances[$path] = $page; + } + $route = $page->route($route); + $parent = $page->parent(); + if ($parent) { + $this->children[$parent->path() ?? ''][$path] = ['slug' => $page->slug()]; + } + $this->routes[$route] = $path; + + $this->grav->fireEvent('onPageProcessed', new Event(['page' => $page])); + } + + /** + * Get a collection of pages in the given context. + * + * @param array $params + * @param array $context + * @return PageCollectionInterface|Collection + */ + public function getCollection(array $params = [], array $context = []) + { + if (!isset($params['items'])) { + return new Collection(); + } + + /** @var Config $config */ + $config = $this->grav['config']; + + $context += [ + 'event' => true, + 'pagination' => true, + 'url_taxonomy_filters' => $config->get('system.pages.url_taxonomy_filters'), + 'taxonomies' => (array)$config->get('site.taxonomies'), + 'pagination_page' => 1, + 'self' => null, + ]; + + // Include taxonomies from the URL if requested. + $process_taxonomy = $params['url_taxonomy_filters'] ?? $context['url_taxonomy_filters']; + if ($process_taxonomy) { + /** @var Uri $uri */ + $uri = $this->grav['uri']; + foreach ($context['taxonomies'] as $taxonomy) { + $param = $uri->param(rawurlencode($taxonomy)); + $items = is_string($param) ? explode(',', $param) : []; + foreach ($items as $item) { + $params['taxonomies'][$taxonomy][] = htmlspecialchars_decode(rawurldecode($item), ENT_QUOTES); + } + } + } + + $pagination = $params['pagination'] ?? $context['pagination']; + if ($pagination && !isset($params['page'], $params['start'])) { + /** @var Uri $uri */ + $uri = $this->grav['uri']; + $context['current_page'] = $uri->currentPage(); + } + + $collection = $this->evaluate($params['items'], $context['self']); + $collection->setParams($params); + + // Filter by taxonomies. + foreach ($params['taxonomies'] ?? [] as $taxonomy => $items) { + foreach ($collection as $page) { + // Don't include modules + if ($page->isModule()) { + continue; + } + + $test = $page->taxonomy()[$taxonomy] ?? []; + foreach ($items as $item) { + if (!$test || !in_array($item, $test, true)) { + $collection->remove($page->path()); + } + } + } + } + + $filters = $params['filter'] ?? []; + + // Assume published=true if not set. + if (!isset($filters['published']) && !isset($filters['non-published'])) { + $filters['published'] = true; + } + + // Remove any inclusive sets from filter. + $sets = ['published', 'visible', 'modular', 'routable']; + foreach ($sets as $type) { + $nonType = "non-{$type}"; + if (isset($filters[$type], $filters[$nonType]) && $filters[$type] === $filters[$nonType]) { + if (!$filters[$type]) { + // Both options are false, return empty collection as nothing can match the filters. + return new Collection(); + } + + // Both options are true, remove opposite filters as all pages will match the filters. + unset($filters[$type], $filters[$nonType]); + } + } + + // Filter the collection + foreach ($filters as $type => $filter) { + if (null === $filter) { + continue; + } + + // Convert non-type to type. + if (str_starts_with($type, 'non-')) { + $type = substr($type, 4); + $filter = !$filter; + } + + switch ($type) { + case 'translated': + if ($filter) { + $collection = $collection->translated(); + } else { + $collection = $collection->nonTranslated(); + } + break; + case 'published': + if ($filter) { + $collection = $collection->published(); + } else { + $collection = $collection->nonPublished(); + } + break; + case 'visible': + if ($filter) { + $collection = $collection->visible(); + } else { + $collection = $collection->nonVisible(); + } + break; + case 'page': + if ($filter) { + $collection = $collection->pages(); + } else { + $collection = $collection->modules(); + } + break; + case 'module': + case 'modular': + if ($filter) { + $collection = $collection->modules(); + } else { + $collection = $collection->pages(); + } + break; + case 'routable': + if ($filter) { + $collection = $collection->routable(); + } else { + $collection = $collection->nonRoutable(); + } + break; + case 'type': + $collection = $collection->ofType($filter); + break; + case 'types': + $collection = $collection->ofOneOfTheseTypes($filter); + break; + case 'access': + $collection = $collection->ofOneOfTheseAccessLevels($filter); + break; + } + } + + if (isset($params['dateRange'])) { + $start = $params['dateRange']['start'] ?? null; + $end = $params['dateRange']['end'] ?? null; + $field = $params['dateRange']['field'] ?? null; + $collection = $collection->dateRange($start, $end, $field); + } + + if (isset($params['order'])) { + $by = $params['order']['by'] ?? 'default'; + $dir = $params['order']['dir'] ?? 'asc'; + $custom = $params['order']['custom'] ?? null; + $sort_flags = $params['order']['sort_flags'] ?? null; + + if (is_array($sort_flags)) { + $sort_flags = array_map('constant', $sort_flags); //transform strings to constant value + $sort_flags = array_reduce($sort_flags, static function ($a, $b) { + return $a | $b; + }, 0); //merge constant values using bit or + } + + $collection = $collection->order($by, $dir, $custom, $sort_flags); + } + + // New Custom event to handle things like pagination. + if ($context['event']) { + $this->grav->fireEvent('onCollectionProcessed', new Event(['collection' => $collection, 'context' => $context])); + } + + if ($context['pagination']) { + // Slice and dice the collection if pagination is required + $params = $collection->params(); + + $limit = (int)($params['limit'] ?? 0); + $page = (int)($params['page'] ?? $context['current_page'] ?? 0); + $start = (int)($params['start'] ?? 0); + $start = $limit > 0 && $page > 0 ? ($page - 1) * $limit : max(0, $start); + + if ($start || ($limit && $collection->count() > $limit)) { + $collection->slice($start, $limit ?: null); + } + } + + return $collection; + } + + /** + * @param array|string $value + * @param PageInterface|null $self + * @return Collection + */ + protected function evaluate($value, PageInterface $self = null) + { + // Parse command. + if (is_string($value)) { + // Format: @command.param + $cmd = $value; + $params = []; + } elseif (is_array($value) && count($value) === 1 && !is_int(key($value))) { + // Format: @command.param: { attr1: value1, attr2: value2 } + $cmd = (string)key($value); + $params = (array)current($value); + } else { + $result = []; + foreach ((array)$value as $key => $val) { + if (is_int($key)) { + $result = $result + $this->evaluate($val, $self)->toArray(); + } else { + $result = $result + $this->evaluate([$key => $val], $self)->toArray(); + } + } + + return new Collection($result); + } + + $parts = explode('.', $cmd); + $scope = array_shift($parts); + $type = $parts[0] ?? null; + + /** @var PageInterface|null $page */ + $page = null; + switch ($scope) { + case 'self@': + case '@self': + $page = $self; + break; + + case 'page@': + case '@page': + $page = isset($params[0]) ? $this->find($params[0]) : null; + break; + + case 'root@': + case '@root': + $page = $this->root(); + break; + + case 'taxonomy@': + case '@taxonomy': + // Gets a collection of pages by using one of the following formats: + // @taxonomy.category: blog + // @taxonomy.category: [ blog, featured ] + // @taxonomy: { category: [ blog, featured ], level: 1 } + + /** @var Taxonomy $taxonomy_map */ + $taxonomy_map = Grav::instance()['taxonomy']; + + if (!empty($parts)) { + $params = [implode('.', $parts) => $params]; + } + + return $taxonomy_map->findTaxonomy($params); + } + + if (!$page) { + return new Collection(); + } + + // Handle '@page', '@page.modular: false', '@self' and '@self.modular: false'. + if (null === $type || (in_array($type, ['modular', 'modules']) && ($params[0] ?? null) === false)) { + $type = 'children'; + } + + switch ($type) { + case 'all': + $collection = $page->children(); + break; + case 'modules': + case 'modular': + $collection = $page->children()->modules(); + break; + case 'pages': + case 'children': + $collection = $page->children()->pages(); + break; + case 'page': + case 'self': + $collection = !$page->root() ? (new Collection())->addPage($page) : new Collection(); + break; + case 'parent': + $parent = $page->parent(); + $collection = new Collection(); + $collection = $parent ? $collection->addPage($parent) : $collection; + break; + case 'siblings': + $parent = $page->parent(); + $collection = $parent ? $parent->children()->remove($page->path()) : new Collection(); + break; + case 'descendants': + $collection = $this->all($page)->remove($page->path())->pages(); + break; + default: + // Unknown type; return empty collection. + $collection = new Collection(); + break; + } + + return $collection; + } + + /** + * Sort sub-pages in a page. + * + * @param PageInterface $page + * @param string|null $order_by + * @param string|null $order_dir + * @return array + */ + public function sort(PageInterface $page, $order_by = null, $order_dir = null, $sort_flags = null) + { + if ($order_by === null) { + $order_by = $page->orderBy(); + } + if ($order_dir === null) { + $order_dir = $page->orderDir(); + } + + $path = $page->path(); + if (null === $path) { + return []; + } + + $children = $this->children[$path] ?? []; + + if (!$children) { + return $children; + } + + if (!isset($this->sort[$path][$order_by])) { + $this->buildSort($path, $children, $order_by, $page->orderManual(), $sort_flags); + } + + $sort = $this->sort[$path][$order_by]; + + if ($order_dir !== 'asc') { + $sort = array_reverse($sort); + } + + return $sort; + } + + /** + * @param Collection $collection + * @param string $orderBy + * @param string $orderDir + * @param array|null $orderManual + * @param int|null $sort_flags + * @return array + * @internal + */ + public function sortCollection(Collection $collection, $orderBy, $orderDir = 'asc', $orderManual = null, $sort_flags = null) + { + $items = $collection->toArray(); + if (!$items) { + return []; + } + + $lookup = md5(json_encode($items) . json_encode($orderManual) . $orderBy . $orderDir); + if (!isset($this->sort[$lookup][$orderBy])) { + $this->buildSort($lookup, $items, $orderBy, $orderManual, $sort_flags); + } + + $sort = $this->sort[$lookup][$orderBy]; + + if ($orderDir !== 'asc') { + $sort = array_reverse($sort); + } + + return $sort; + } + + /** + * Get a page instance. + * + * @param string $path The filesystem full path of the page + * @return PageInterface|null + * @throws RuntimeException + */ + public function get($path) + { + $path = (string)$path; + if ($path === '') { + return null; + } + + // Check for local instances first. + if (array_key_exists($path, $this->instances)) { + return $this->instances[$path]; + } + + $instance = $this->index[$path] ?? null; + if (is_string($instance)) { + if ($this->directory) { + /** @var Language $language */ + $language = $this->grav['language']; + $lang = $language->getActive(); + if ($lang) { + $languages = $language->getFallbackLanguages($lang, true); + $key = $instance; + $instance = null; + foreach ($languages as $code) { + $test = $code ? $key . ':' . $code : $key; + if (($instance = $this->directory->getObject($test, 'flex_key')) !== null) { + break; + } + } + } else { + $instance = $this->directory->getObject($instance, 'flex_key'); + } + } + + if ($instance instanceof PageInterface) { + if ($this->fire_events && method_exists($instance, 'initialize')) { + $instance->initialize(); + } + } else { + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addMessage(sprintf('Flex page %s is missing or broken!', $instance), 'debug'); + } + } + + if ($instance) { + $this->instances[$path] = $instance; + } + + return $instance; + } + + /** + * Get children of the path. + * + * @param string $path + * @return Collection + */ + public function children($path) + { + $children = $this->children[(string)$path] ?? []; + + return new Collection($children, [], $this); + } + + /** + * Get a page ancestor. + * + * @param string $route The relative URL of the page + * @param string|null $path The relative path of the ancestor folder + * @return PageInterface|null + */ + public function ancestor($route, $path = null) + { + if ($path !== null) { + $page = $this->find($route, true); + + if ($page && $page->path() === $path) { + return $page; + } + + $parent = $page ? $page->parent() : null; + if ($parent && !$parent->root()) { + return $this->ancestor($parent->route(), $path); + } + } + + return null; + } + + /** + * Get a page ancestor trait. + * + * @param string $route The relative route of the page + * @param string|null $field The field name of the ancestor to query for + * @return PageInterface|null + */ + public function inherited($route, $field = null) + { + if ($field !== null) { + $page = $this->find($route, true); + + $parent = $page ? $page->parent() : null; + if ($parent && $parent->value('header.' . $field) !== null) { + return $parent; + } + if ($parent && !$parent->root()) { + return $this->inherited($parent->route(), $field); + } + } + + return null; + } + + /** + * Find a page based on route. + * + * @param string $route The route of the page + * @param bool $all If true, return also non-routable pages, otherwise return null if page isn't routable + * @return PageInterface|null + */ + public function find($route, $all = false) + { + $route = urldecode((string)$route); + + // Fetch page if there's a defined route to it. + $path = $this->routes[$route] ?? null; + $page = null !== $path ? $this->get($path) : null; + + // Try without trailing slash + if (null === $page && Utils::endsWith($route, '/')) { + $path = $this->routes[rtrim($route, '/')] ?? null; + $page = null !== $path ? $this->get($path) : null; + } + + if (!$all && !isset($this->grav['admin'])) { + if (null === $page || !$page->routable()) { + // If the page cannot be accessed, look for the site wide routes and wildcards. + $page = $this->findSiteBasedRoute($route) ?? $page; + } + } + + return $page; + } + + /** + * Check site based routes. + * + * @param string $route + * @return PageInterface|null + */ + protected function findSiteBasedRoute($route) + { + /** @var Config $config */ + $config = $this->grav['config']; + + $site_routes = $config->get('site.routes'); + if (!is_array($site_routes)) { + return null; + } + + $page = null; + + // See if route matches one in the site configuration + $site_route = $site_routes[$route] ?? null; + if ($site_route) { + $page = $this->find($site_route); + } else { + // Use reverse order because of B/C (previously matched multiple and returned the last match). + foreach (array_reverse($site_routes, true) as $pattern => $replace) { + $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#'; + try { + $found = preg_replace($pattern, $replace, $route); + if ($found && $found !== $route) { + $page = $this->find($found); + if ($page) { + return $page; + } + } + } catch (ErrorException $e) { + $this->grav['log']->error('site.routes: ' . $pattern . '-> ' . $e->getMessage()); + } + } + } + + return $page; + } + + /** + * Dispatch URI to a page. + * + * @param string $route The relative URL of the page + * @param bool $all If true, return also non-routable pages, otherwise return null if page isn't routable + * @param bool $redirect If true, allow redirects + * @return PageInterface|null + * @throws Exception + */ + public function dispatch($route, $all = false, $redirect = true) + { + $page = $this->find($route, true); + + // If we want all pages or are in admin, return what we already have. + if ($all || isset($this->grav['admin'])) { + return $page; + } + + if ($page) { + $routable = $page->routable(); + if ($redirect) { + if ($page->redirect()) { + // Follow a redirect page. + $this->grav->redirectLangSafe($page->redirect()); + } + + if (!$routable && ($child = $page->children()->visible()->routable()->published()->first()) !== null) { + // Redirect to the first visible child as current page isn't routable. + $this->grav->redirectLangSafe($child->route()); + } + } + + if ($routable) { + return $page; + } + } + + $route = urldecode((string)$route); + + // The page cannot be reached, look into site wide redirects, routes and wildcards. + $redirectedPage = $this->findSiteBasedRoute($route); + if ($redirectedPage) { + $page = $this->dispatch($redirectedPage->route(), false, $redirect); + } + + /** @var Config $config */ + $config = $this->grav['config']; + + /** @var Uri $uri */ + $uri = $this->grav['uri']; + /** @var \Grav\Framework\Uri\Uri $source_url */ + $source_url = $uri->uri(false); + + // Try Regex style redirects + $site_redirects = $config->get('site.redirects'); + if (is_array($site_redirects)) { + foreach ((array)$site_redirects as $pattern => $replace) { + $pattern = ltrim($pattern, '^'); + $pattern = '#^' . str_replace('/', '\/', $pattern) . '#'; + try { + /** @var string $found */ + $found = preg_replace($pattern, $replace, $source_url); + if ($found && $found !== $source_url) { + $this->grav->redirectLangSafe($found); + } + } catch (ErrorException $e) { + $this->grav['log']->error('site.redirects: ' . $pattern . '-> ' . $e->getMessage()); + } + } + } + + return $page; + } + + /** + * Get root page. + * + * @return PageInterface + * @throws RuntimeException + */ + public function root() + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + $path = $locator->findResource('page://'); + $root = is_string($path) ? $this->get(rtrim($path, '/')) : null; + if (null === $root) { + throw new RuntimeException('Internal error'); + } + + return $root; + } + + /** + * Get a blueprint for a page type. + * + * @param string $type + * @return Blueprint + */ + public function blueprints($type) + { + if ($this->blueprints === null) { + $this->blueprints = new Blueprints(self::getTypes()); + } + + try { + $blueprint = $this->blueprints->get($type); + } catch (RuntimeException $e) { + $blueprint = $this->blueprints->get('default'); + } + + if (empty($blueprint->initialized)) { + $blueprint->initialized = true; + $this->grav->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $type])); + } + + return $blueprint; + } + + /** + * Get all pages + * + * @param PageInterface|null $current + * @return Collection + */ + public function all(PageInterface $current = null) + { + $all = new Collection(); + + /** @var PageInterface $current */ + $current = $current ?: $this->root(); + + if (!$current->root()) { + $all[$current->path()] = ['slug' => $current->slug()]; + } + + foreach ($current->children() as $next) { + $all->append($this->all($next)); + } + + return $all; + } + + /** + * Get available parents raw routes. + * + * @return array + */ + public static function parentsRawRoutes() + { + $rawRoutes = true; + + return self::getParents($rawRoutes); + } + + /** + * Get available parents routes + * + * @param bool $rawRoutes get the raw route or the normal route + * @return array + */ + private static function getParents($rawRoutes) + { + $grav = Grav::instance(); + + /** @var Pages $pages */ + $pages = $grav['pages']; + + $parents = $pages->getList(null, 0, $rawRoutes); + + if (isset($grav['admin'])) { + // Remove current route from parents + + /** @var Admin $admin */ + $admin = $grav['admin']; + + $page = $admin->getPage($admin->route); + $page_route = $page->route(); + if (isset($parents[$page_route])) { + unset($parents[$page_route]); + } + } + + return $parents; + } + + /** + * Get list of route/title of all pages. Title is in HTML. + * + * @param PageInterface|null $current + * @param int $level + * @param bool $rawRoutes + * @param bool $showAll + * @param bool $showFullpath + * @param bool $showSlug + * @param bool $showModular + * @param bool $limitLevels + * @return array + */ + public function getList(PageInterface $current = null, $level = 0, $rawRoutes = false, $showAll = true, $showFullpath = false, $showSlug = false, $showModular = false, $limitLevels = false) + { + if (!$current) { + if ($level) { + throw new RuntimeException('Internal error'); + } + + $current = $this->root(); + } + + $list = []; + + if (!$current->root()) { + if ($rawRoutes) { + $route = $current->rawRoute(); + } else { + $route = $current->route(); + } + + if ($showFullpath) { + $option = htmlspecialchars($current->route()); + } else { + $extra = $showSlug ? '(' . $current->slug() . ') ' : ''; + $option = str_repeat('—-', $level). '▸ ' . $extra . htmlspecialchars($current->title()); + } + + $list[$route] = $option; + } + + if ($limitLevels === false || ($level+1 < $limitLevels)) { + foreach ($current->children() as $next) { + if ($showAll || $next->routable() || ($next->isModule() && $showModular)) { + $list = array_merge($list, $this->getList($next, $level + 1, $rawRoutes, $showAll, $showFullpath, $showSlug, $showModular, $limitLevels)); + } + } + } + + return $list; + } + + /** + * Get available page types. + * + * @return Types + */ + public static function getTypes() + { + if (null === self::$types) { + $grav = Grav::instance(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + // Prevent calls made before theme:// has been initialized (happens when upgrading old version of Admin plugin). + if (!$locator->isStream('theme://')) { + return new Types(); + } + + $scanBlueprintsAndTemplates = static function (Types $types) use ($grav) { + // Scan blueprints + $event = new Event(); + $event->types = $types; + $grav->fireEvent('onGetPageBlueprints', $event); + + $types->init(); + + // Try new location first. + $lookup = 'theme://blueprints/pages/'; + if (!is_dir($lookup)) { + $lookup = 'theme://blueprints/'; + } + $types->scanBlueprints($lookup); + + // Scan templates + $event = new Event(); + $event->types = $types; + $grav->fireEvent('onGetPageTemplates', $event); + + $types->scanTemplates('theme://templates/'); + }; + + if ($grav['config']->get('system.cache.enabled')) { + /** @var Cache $cache */ + $cache = $grav['cache']; + + // Use cached types if possible. + $types_cache_id = md5('types'); + $types = $cache->fetch($types_cache_id); + + if (!$types instanceof Types) { + $types = new Types(); + $scanBlueprintsAndTemplates($types); + $cache->save($types_cache_id, $types); + } + } else { + $types = new Types(); + $scanBlueprintsAndTemplates($types); + } + + // Register custom paths to the locator. + $locator = $grav['locator']; + foreach ($types as $type => $paths) { + foreach ($paths as $k => $path) { + if (strpos($path, 'blueprints://') === 0) { + unset($paths[$k]); + } + } + if ($paths) { + $locator->addPath('blueprints', "pages/$type.yaml", $paths); + } + } + + self::$types = $types; + } + + return self::$types; + } + + /** + * Get available page types. + * + * @return array + */ + public static function types() + { + $types = self::getTypes(); + + return $types->pageSelect(); + } + + /** + * Get available page types. + * + * @return array + */ + public static function modularTypes() + { + $types = self::getTypes(); + + return $types->modularSelect(); + } + + /** + * Get template types based on page type (standard or modular) + * + * @param string|null $type + * @return array + */ + public static function pageTypes($type = null) + { + if (null === $type && isset(Grav::instance()['admin'])) { + /** @var Admin $admin */ + $admin = Grav::instance()['admin']; + + /** @var PageInterface|null $page */ + $page = $admin->page(); + + $type = $page && $page->isModule() ? 'modular' : 'standard'; + } + + switch ($type) { + case 'standard': + return static::types(); + case 'modular': + return static::modularTypes(); + } + + return []; + } + + /** + * Get access levels of the site pages + * + * @return array + */ + public function accessLevels() + { + $accessLevels = []; + foreach ($this->all() as $page) { + if ($page instanceof PageInterface && isset($page->header()->access)) { + if (is_array($page->header()->access)) { + foreach ($page->header()->access as $index => $accessLevel) { + if (is_array($accessLevel)) { + foreach ($accessLevel as $innerIndex => $innerAccessLevel) { + $accessLevels[] = $innerIndex; + } + } else { + $accessLevels[] = $index; + } + } + } else { + $accessLevels[] = $page->header()->access; + } + } + } + + return array_unique($accessLevels); + } + + /** + * Get available parents routes + * + * @return array + */ + public static function parents() + { + $rawRoutes = false; + + return self::getParents($rawRoutes); + } + + /** + * Gets the home route + * + * @return string + */ + public static function getHomeRoute() + { + if (empty(self::$home_route)) { + $grav = Grav::instance(); + + /** @var Config $config */ + $config = $grav['config']; + + /** @var Language $language */ + $language = $grav['language']; + + $home = $config->get('system.home.alias'); + + if ($language->enabled()) { + $home_aliases = $config->get('system.home.aliases'); + if ($home_aliases) { + $active = $language->getActive(); + $default = $language->getDefault(); + + try { + if ($active) { + $home = $home_aliases[$active]; + } else { + $home = $home_aliases[$default]; + } + } catch (ErrorException $e) { + $home = $home_aliases[$default]; + } + } + } + + self::$home_route = trim($home, '/'); + } + + return self::$home_route; + } + + /** + * Needed for testing where we change the home route via config + * + * @return string|null + */ + public static function resetHomeRoute() + { + self::$home_route = null; + + return self::getHomeRoute(); + } + + protected function initFlexPages(): void + { + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addMessage('Pages: Flex Directory'); + + /** @var Flex $flex */ + $flex = $this->grav['flex']; + $directory = $flex->getDirectory('pages'); + + /** @var EventDispatcher $dispatcher */ + $dispatcher = $this->grav['events']; + + // Stop /admin/pages from working, display error instead. + $dispatcher->addListener( + 'onAdminPage', + static function (Event $event) use ($directory) { + $grav = Grav::instance(); + $admin = $grav['admin']; + [$base,$location,] = $admin->getRouteDetails(); + if ($location !== 'pages' || isset($grav['flex_objects'])) { + return; + } + + /** @var PageInterface $page */ + $page = $event['page']; + $page->init(new SplFileInfo('plugin://admin/pages/admin/error.md')); + $page->routable(true); + $header = $page->header(); + $header->title = 'Please install missing plugin'; + $page->content("## Please install and enable **[Flex Objects]({$base}/plugins/flex-objects)** plugin. It is required to edit **Flex Pages**."); + + /** @var Header $header */ + $header = $page->header(); + $menu = $directory->getConfig('admin.menu.list'); + $header->access = $menu['authorize'] ?? ['admin.super']; + }, + 100000 + ); + + $this->directory = $directory; + } + + /** + * Builds pages. + * + * @internal + */ + protected function buildPages(): void + { + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->startTimer('build-pages', 'Init frontend routes'); + + if ($this->directory) { + $this->buildFlexPages($this->directory); + } else { + $this->buildRegularPages(); + } + $debugger->stopTimer('build-pages'); + } + + protected function buildFlexPages(FlexDirectory $directory): void + { + /** @var Config $config */ + $config = $this->grav['config']; + + // TODO: right now we are just emulating normal pages, it is inefficient and bad... but works! + /** @var PageCollection|PageIndex $collection */ + $collection = $directory->getIndex(null, 'storage_key'); + $cache = $directory->getCache('index'); + + /** @var Language $language */ + $language = $this->grav['language']; + + $this->pages_cache_id = 'pages-' . md5($collection->getCacheChecksum() . $language->getActive() . $config->checksum()); + + $cached = $cache->get($this->pages_cache_id); + + if ($cached && $this->getVersion() === $cached[0]) { + [, $this->index, $this->routes, $this->children, $taxonomy_map, $this->sort] = $cached; + + /** @var Taxonomy $taxonomy */ + $taxonomy = $this->grav['taxonomy']; + $taxonomy->taxonomy($taxonomy_map); + + return; + } + + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addMessage('Page cache missed, rebuilding Flex Pages..'); + + $root = $collection->getRoot(); + $root_path = $root->path(); + $this->routes = []; + $this->instances = [$root_path => $root]; + $this->index = [$root_path => $root]; + $this->children = []; + $this->sort = []; + + if ($this->fire_events) { + $this->grav->fireEvent('onBuildPagesInitialized'); + } + + /** @var PageInterface $page */ + foreach ($collection as $page) { + $path = $page->path(); + if (null === $path) { + throw new RuntimeException('Internal error'); + } + + if ($page instanceof FlexTranslateInterface) { + $page = $page->hasTranslation() ? $page->getTranslation() : null; + } + + if (!$page instanceof FlexPageObject || $path === $root_path) { + continue; + } + + if ($this->fire_events) { + if (method_exists($page, 'initialize')) { + $page->initialize(); + } else { + // TODO: Deprecated, only used in 1.7 betas. + $this->grav->fireEvent('onPageProcessed', new Event(['page' => $page])); + } + } + + $parent = dirname($path); + + $route = $page->rawRoute(); + + // Skip duplicated empty folders (git revert does not remove those). + // TODO: still not perfect, will only work if the page has been translated. + if (isset($this->routes[$route])) { + $oldPath = $this->routes[$route]; + if ($page->isPage()) { + unset($this->index[$oldPath], $this->children[dirname($oldPath)][$oldPath]); + } else { + continue; + } + } + + $this->routes[$route] = $path; + $this->instances[$path] = $page; + $this->index[$path] = $page->getFlexKey(); + // FIXME: ... better... + $this->children[$parent][$path] = ['slug' => $page->slug()]; + if (!isset($this->children[$path])) { + $this->children[$path] = []; + } + } + + foreach ($this->children as $path => $list) { + $page = $this->instances[$path] ?? null; + if (null === $page) { + continue; + } + // Call onFolderProcessed event. + if ($this->fire_events) { + $this->grav->fireEvent('onFolderProcessed', new Event(['page' => $page])); + } + // Sort the children. + $this->children[$path] = $this->sort($page); + } + + $this->routes = []; + $this->buildRoutes(); + + // cache if needed + if (null !== $cache) { + /** @var Taxonomy $taxonomy */ + $taxonomy = $this->grav['taxonomy']; + $taxonomy_map = $taxonomy->taxonomy(); + + // save pages, routes, taxonomy, and sort to cache + $cache->set($this->pages_cache_id, [$this->getVersion(), $this->index, $this->routes, $this->children, $taxonomy_map, $this->sort]); + } + } + + /** + * @return Page + */ + protected function buildRootPage() + { + $grav = Grav::instance(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + $path = $locator->findResource('page://'); + if (!is_string($path)) { + throw new RuntimeException('Internal Error'); + } + + /** @var Config $config */ + $config = $grav['config']; + + $page = new Page(); + $page->path($path); + $page->orderDir($config->get('system.pages.order.dir')); + $page->orderBy($config->get('system.pages.order.by')); + $page->modified(0); + $page->routable(false); + $page->template('default'); + $page->extension('.md'); + + return $page; + } + + protected function buildRegularPages(): void + { + /** @var Config $config */ + $config = $this->grav['config']; + + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + /** @var Language $language */ + $language = $this->grav['language']; + + $pages_dir = $locator->findResource('page://'); + if (!is_string($pages_dir)) { + throw new RuntimeException('Internal Error'); + } + + // Set active language + $this->active_lang = $language->getActive(); + + if ($config->get('system.cache.enabled')) { + /** @var Language $language */ + $language = $this->grav['language']; + + // how should we check for last modified? Default is by file + switch ($this->check_method) { + case 'none': + case 'off': + $hash = 0; + break; + case 'folder': + $hash = Folder::lastModifiedFolder($pages_dir); + break; + case 'hash': + $hash = Folder::hashAllFiles($pages_dir); + break; + default: + $hash = Folder::lastModifiedFile($pages_dir); + } + + $this->pages_cache_id = md5($pages_dir . $hash . $language->getActive() . $config->checksum()); + + /** @var Cache $cache */ + $cache = $this->grav['cache']; + $cached = $cache->fetch($this->pages_cache_id); + if ($cached && $this->getVersion() === $cached[0]) { + [, $this->index, $this->routes, $this->children, $taxonomy_map, $this->sort] = $cached; + + /** @var Taxonomy $taxonomy */ + $taxonomy = $this->grav['taxonomy']; + $taxonomy->taxonomy($taxonomy_map); + + return; + } + + $this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..'); + } else { + $this->grav['debugger']->addMessage('Page cache disabled, rebuilding pages..'); + } + + $this->resetPages($pages_dir); + } + + /** + * Accessible method to manually reset the pages cache + * + * @param string $pages_dir + */ + public function resetPages($pages_dir): void + { + $this->sort = []; + $this->recurse($pages_dir); + $this->buildRoutes(); + + // cache if needed + if ($this->grav['config']->get('system.cache.enabled')) { + /** @var Cache $cache */ + $cache = $this->grav['cache']; + /** @var Taxonomy $taxonomy */ + $taxonomy = $this->grav['taxonomy']; + + // save pages, routes, taxonomy, and sort to cache + $cache->save($this->pages_cache_id, [$this->getVersion(), $this->index, $this->routes, $this->children, $taxonomy->taxonomy(), $this->sort]); + } + } + + /** + * Recursive function to load & build page relationships. + * + * @param string $directory + * @param PageInterface|null $parent + * @return PageInterface + * @throws RuntimeException + * @internal + */ + protected function recurse($directory, PageInterface $parent = null) + { + $directory = rtrim($directory, DS); + $page = new Page; + + /** @var Config $config */ + $config = $this->grav['config']; + + /** @var Language $language */ + $language = $this->grav['language']; + + // Stuff to do at root page + // Fire event for memory and time consuming plugins... + if ($parent === null && $this->fire_events) { + $this->grav->fireEvent('onBuildPagesInitialized'); + } + + $page->path($directory); + if ($parent) { + $page->parent($parent); + } + + $page->orderDir($config->get('system.pages.order.dir')); + $page->orderBy($config->get('system.pages.order.by')); + + // Add into instances + if (!isset($this->index[$page->path()])) { + $this->index[$page->path()] = $page; + $this->instances[$page->path()] = $page; + if ($parent && $page->path()) { + $this->children[$parent->path()][$page->path()] = ['slug' => $page->slug()]; + } + } elseif ($parent !== null) { + throw new RuntimeException('Fatal error when creating page instances.'); + } + + // Build regular expression for all the allowed page extensions. + $page_extensions = $language->getFallbackPageExtensions(); + $regex = '/^[^\.]*(' . implode('|', array_map( + static function ($str) { + return preg_quote($str, '/'); + }, + $page_extensions + )) . ')$/'; + + $folders = []; + $page_found = null; + $page_extension = '.md'; + $last_modified = 0; + + $iterator = new FilesystemIterator($directory); + foreach ($iterator as $file) { + $filename = $file->getFilename(); + + // Ignore all hidden files if set. + if ($this->ignore_hidden && $filename && strpos($filename, '.') === 0) { + continue; + } + + // Handle folders later. + if ($file->isDir()) { + // But ignore all folders in ignore list. + if (!in_array($filename, $this->ignore_folders, true)) { + $folders[] = $file; + } + continue; + } + + // Ignore all files in ignore list. + if (in_array($filename, $this->ignore_files, true)) { + continue; + } + + // Update last modified date to match the last updated file in the folder. + $modified = $file->getMTime(); + if ($modified > $last_modified) { + $last_modified = $modified; + } + + // Page is the one that matches to $page_extensions list with the lowest index number. + if (preg_match($regex, $filename, $matches, PREG_OFFSET_CAPTURE)) { + $ext = $matches[1][0]; + + if ($page_found === null || array_search($ext, $page_extensions, true) < array_search($page_extension, $page_extensions, true)) { + $page_found = $file; + $page_extension = $ext; + } + } + } + + $content_exists = false; + if ($parent && $page_found) { + $page->init($page_found, $page_extension); + + $content_exists = true; + + if ($this->fire_events) { + $this->grav->fireEvent('onPageProcessed', new Event(['page' => $page])); + } + } + + // Now handle all the folders under the page. + /** @var FilesystemIterator $file */ + foreach ($folders as $file) { + $filename = $file->getFilename(); + + // if folder contains separator, continue + if (Utils::contains($file->getFilename(), $config->get('system.param_sep', ':'))) { + continue; + } + + if (!$page->path()) { + $page->path($file->getPath()); + } + + $path = $directory . DS . $filename; + $child = $this->recurse($path, $page); + + if (preg_match('/^(\d+\.)_/', $filename)) { + $child->routable(false); + $child->modularTwig(true); + } + + $this->children[$page->path()][$child->path()] = ['slug' => $child->slug()]; + + if ($this->fire_events) { + $this->grav->fireEvent('onFolderProcessed', new Event(['page' => $page])); + } + } + + if (!$content_exists) { + // Set routable to false if no page found + $page->routable(false); + + // Hide empty folders if option set + if ($config->get('system.pages.hide_empty_folders')) { + $page->visible(false); + } + } + + // Override the modified time if modular + if ($page->template() === 'modular') { + foreach ($page->collection() as $child) { + $modified = $child->modified(); + + if ($modified > $last_modified) { + $last_modified = $modified; + } + } + } + + // Override the modified and ID so that it takes the latest change into account + $page->modified($last_modified); + $page->id($last_modified . md5($page->filePath() ?? '')); + + // Sort based on Defaults or Page Overridden sort order + $this->children[$page->path()] = $this->sort($page); + + return $page; + } + + /** + * @internal + */ + protected function buildRoutes(): void + { + /** @var Taxonomy $taxonomy */ + $taxonomy = $this->grav['taxonomy']; + + // Get the home route + $home = self::resetHomeRoute(); + // Build routes and taxonomy map. + /** @var PageInterface|string $page */ + foreach ($this->index as $path => $page) { + if (is_string($page)) { + $page = $this->get($path); + } + + if (!$page || $page->root()) { + continue; + } + + // process taxonomy + $taxonomy->addTaxonomy($page); + + $page_path = $page->path(); + if (null === $page_path) { + throw new RuntimeException('Internal Error'); + } + + $route = $page->route(); + $raw_route = $page->rawRoute(); + + // add regular route + if ($route) { + $this->routes[$route] = $page_path; + } + + // add raw route + if ($raw_route && $raw_route !== $route) { + $this->routes[$raw_route] = $page_path; + } + + // add canonical route + $route_canonical = $page->routeCanonical(); + if ($route_canonical && $route !== $route_canonical) { + $this->routes[$route_canonical] = $page_path; + } + + // add aliases to routes list if they are provided + $route_aliases = $page->routeAliases(); + if ($route_aliases) { + foreach ($route_aliases as $alias) { + $this->routes[$alias] = $page_path; + } + } + } + + // Alias and set default route to home page. + $homeRoute = "/{$home}"; + if ($home && isset($this->routes[$homeRoute])) { + $home = $this->get($this->routes[$homeRoute]); + if ($home) { + $this->routes['/'] = $this->routes[$homeRoute]; + $home->route('/'); + } + } + } + + /** + * @param string $path + * @param array $pages + * @param string $order_by + * @param array|null $manual + * @param int|null $sort_flags + * @throws RuntimeException + * @internal + */ + protected function buildSort($path, array $pages, $order_by = 'default', $manual = null, $sort_flags = null): void + { + $list = []; + $header_query = null; + $header_default = null; + + // do this header query work only once + if (strpos($order_by, 'header.') === 0) { + $query = explode('|', str_replace('header.', '', $order_by), 2); + $header_query = array_shift($query) ?? ''; + $header_default = array_shift($query); + } + + foreach ($pages as $key => $info) { + $child = $this->get($key); + if (!$child) { + throw new RuntimeException("Page does not exist: {$key}"); + } + + switch ($order_by) { + case 'title': + $list[$key] = $child->title(); + break; + case 'date': + $list[$key] = $child->date(); + $sort_flags = SORT_REGULAR; + break; + case 'modified': + $list[$key] = $child->modified(); + $sort_flags = SORT_REGULAR; + break; + case 'publish_date': + $list[$key] = $child->publishDate(); + $sort_flags = SORT_REGULAR; + break; + case 'unpublish_date': + $list[$key] = $child->unpublishDate(); + $sort_flags = SORT_REGULAR; + break; + case 'slug': + $list[$key] = $child->slug(); + break; + case 'basename': + $list[$key] = basename($key); + break; + case 'folder': + $list[$key] = $child->folder(); + break; + case 'manual': + case 'default': + default: + if (is_string($header_query)) { + $child_header = $child->header(); + if (!$child_header instanceof Header) { + $child_header = new Header((array)$child_header); + } + $header_value = $child_header->get($header_query); + if (is_array($header_value)) { + $list[$key] = implode(',', $header_value); + } elseif ($header_value) { + $list[$key] = $header_value; + } else { + $list[$key] = $header_default ?: $key; + } + $sort_flags = $sort_flags ?: SORT_REGULAR; + break; + } + $list[$key] = $key; + $sort_flags = $sort_flags ?: SORT_REGULAR; + } + } + + if (!$sort_flags) { + $sort_flags = SORT_NATURAL | SORT_FLAG_CASE; + } + + // handle special case when order_by is random + if ($order_by === 'random') { + $list = $this->arrayShuffle($list); + } else { + // else just sort the list according to specified key + if (extension_loaded('intl') && $this->grav['config']->get('system.intl_enabled')) { + $locale = setlocale(LC_COLLATE, '0'); //`setlocale` with a '0' param returns the current locale set + $col = Collator::create($locale); + if ($col) { + $col->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON); + if (($sort_flags & SORT_NATURAL) === SORT_NATURAL) { + $list = preg_replace_callback('~([0-9]+)\.~', static function ($number) { + return sprintf('%032d.', $number[0]); + }, $list); + if (!is_array($list)) { + throw new RuntimeException('Internal Error'); + } + + $list_vals = array_values($list); + if (is_numeric(array_shift($list_vals))) { + $sort_flags = Collator::SORT_REGULAR; + } else { + $sort_flags = Collator::SORT_STRING; + } + } + + $col->asort($list, $sort_flags); + } else { + asort($list, $sort_flags); + } + } else { + asort($list, $sort_flags); + } + } + + + // Move manually ordered items into the beginning of the list. Order of the unlisted items does not change. + if (is_array($manual) && !empty($manual)) { + $new_list = []; + $i = count($manual); + + foreach ($list as $key => $dummy) { + $info = $pages[$key]; + $order = array_search($info['slug'], $manual, true); + if ($order === false) { + $order = $i++; + } + $new_list[$key] = (int)$order; + } + + $list = $new_list; + + // Apply manual ordering to the list. + asort($list, SORT_NUMERIC); + } + + foreach ($list as $key => $sort) { + $info = $pages[$key]; + $this->sort[$path][$order_by][$key] = $info; + } + } + + /** + * Shuffles an associative array + * + * @param array $list + * @return array + */ + protected function arrayShuffle($list) + { + $keys = array_keys($list); + shuffle($keys); + + $new = []; + foreach ($keys as $key) { + $new[$key] = $list[$key]; + } + + return $new; + } + + /** + * @return string + */ + protected function getVersion() + { + return $this->directory ? 'flex' : 'regular'; + } + + /** + * Get the Pages cache ID + * + * this is particularly useful to know if pages have changed and you want + * to sync another cache with pages cache - works best in `onPagesInitialized()` + * + * @return string + */ + public function getPagesCacheId() + { + return $this->pages_cache_id; + } +} diff --git a/system/src/Grav/Common/Page/Traits/PageFormTrait.php b/system/src/Grav/Common/Page/Traits/PageFormTrait.php new file mode 100644 index 0000000..b99e7b7 --- /dev/null +++ b/system/src/Grav/Common/Page/Traits/PageFormTrait.php @@ -0,0 +1,126 @@ + blueprint, ...], where blueprint follows the regular form blueprint format. + * + * @return array + */ + public function getForms(): array + { + if (null === $this->_forms) { + $header = $this->header(); + + // Call event to allow filling the page header form dynamically (e.g. use case: Comments plugin) + $grav = Grav::instance(); + $grav->fireEvent('onFormPageHeaderProcessed', new Event(['page' => $this, 'header' => $header])); + + $rules = $header->rules ?? null; + if (!is_array($rules)) { + $rules = []; + } + + $forms = []; + + // First grab page.header.form + $form = $this->normalizeForm($header->form ?? null, null, $rules); + if ($form) { + $forms[$form['name']] = $form; + } + + // Append page.header.forms (override singular form if it clashes) + $headerForms = $header->forms ?? null; + if (is_array($headerForms)) { + foreach ($headerForms as $name => $form) { + $form = $this->normalizeForm($form, $name, $rules); + if ($form) { + $forms[$form['name']] = $form; + } + } + } + + $this->_forms = $forms; + } + + return $this->_forms; + } + + /** + * Add forms to this page. + * + * @param array $new + * @param bool $override + * @return $this + */ + public function addForms(array $new, $override = true) + { + // Initialize forms. + $this->forms(); + + foreach ($new as $name => $form) { + $form = $this->normalizeForm($form, $name); + $name = $form['name'] ?? null; + if ($name && ($override || !isset($this->_forms[$name]))) { + $this->_forms[$name] = $form; + } + } + + return $this; + } + + /** + * Alias of $this->getForms(); + * + * @return array + */ + public function forms(): array + { + return $this->getForms(); + } + + /** + * @param array|null $form + * @param string|null $name + * @param array $rules + * @return array|null + */ + protected function normalizeForm($form, $name = null, array $rules = []): ?array + { + if (!is_array($form)) { + return null; + } + + // Ignore numeric indexes on name. + if (!$name || (string)(int)$name === (string)$name) { + $name = null; + } + + $name = $name ?? $form['name'] ?? $this->slug(); + + $formRules = $form['rules'] ?? null; + if (!is_array($formRules)) { + $formRules = []; + } + + return ['name' => $name, 'rules' => $rules + $formRules] + $form; + } + + abstract public function header($var = null); + abstract public function slug($var = null); +} diff --git a/system/src/Grav/Common/Page/Types.php b/system/src/Grav/Common/Page/Types.php new file mode 100644 index 0000000..c718368 --- /dev/null +++ b/system/src/Grav/Common/Page/Types.php @@ -0,0 +1,178 @@ +items[$type])) { + $this->items[$type] = []; + } elseif (null === $blueprint) { + return; + } + + if (null === $blueprint) { + $blueprint = $this->systemBlueprints[$type] ?? $this->systemBlueprints['default'] ?? null; + } + + if ($blueprint) { + array_unshift($this->items[$type], $blueprint); + } + } + + /** + * @return void + */ + public function init() + { + if (empty($this->systemBlueprints)) { + // Register all blueprints from the blueprints stream. + $this->systemBlueprints = $this->findBlueprints('blueprints://pages'); + foreach ($this->systemBlueprints as $type => $blueprint) { + $this->register($type); + } + } + } + + /** + * @param string $uri + * @return void + */ + public function scanBlueprints($uri) + { + if (!is_string($uri)) { + throw new InvalidArgumentException('First parameter must be URI'); + } + + foreach ($this->findBlueprints($uri) as $type => $blueprint) { + $this->register($type, $blueprint); + } + } + + /** + * @param string $uri + * @return void + */ + public function scanTemplates($uri) + { + if (!is_string($uri)) { + throw new InvalidArgumentException('First parameter must be URI'); + } + + $options = [ + 'compare' => 'Filename', + 'pattern' => '|\.html\.twig$|', + 'filters' => [ + 'value' => '|\.html\.twig$|' + ], + 'value' => 'Filename', + 'recursive' => false + ]; + + foreach (Folder::all($uri, $options) as $type) { + $this->register($type); + } + + $modular_uri = rtrim($uri, '/') . '/modular'; + if (is_dir($modular_uri)) { + foreach (Folder::all($modular_uri, $options) as $type) { + $this->register('modular/' . $type); + } + } + } + + /** + * @return array + */ + public function pageSelect() + { + $list = []; + foreach ($this->items as $name => $file) { + if (strpos($name, '/')) { + continue; + } + $list[$name] = ucfirst(str_replace('_', ' ', $name)); + } + ksort($list); + + return $list; + } + + /** + * @return array + */ + public function modularSelect() + { + $list = []; + foreach ($this->items as $name => $file) { + if (strpos($name, 'modular/') !== 0) { + continue; + } + $list[$name] = ucfirst(trim(str_replace('_', ' ', basename($name)))); + } + ksort($list); + + return $list; + } + + /** + * @param string $uri + * @return array + */ + private function findBlueprints($uri) + { + $options = [ + 'compare' => 'Filename', + 'pattern' => '|\.yaml$|', + 'filters' => [ + 'key' => '|\.yaml$|' + ], + 'key' => 'SubPathName', + 'value' => 'PathName', + ]; + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + if ($locator->isStream($uri)) { + $options['value'] = 'Url'; + } + + return Folder::all($uri, $options); + } +} diff --git a/system/src/Grav/Common/Plugin.php b/system/src/Grav/Common/Plugin.php new file mode 100644 index 0000000..3b0d3fb --- /dev/null +++ b/system/src/Grav/Common/Plugin.php @@ -0,0 +1,448 @@ +name = $name; + $this->grav = $grav; + + if ($config) { + $this->setConfig($config); + } + } + + /** + * @return ClassLoader|null + * @internal + */ + final public function getAutoloader(): ?ClassLoader + { + return $this->loader; + } + + /** + * @param ClassLoader|null $loader + * @internal + */ + final public function setAutoloader(?ClassLoader $loader): void + { + $this->loader = $loader; + } + + /** + * @param Config $config + * @return $this + */ + public function setConfig(Config $config) + { + $this->config = $config; + + return $this; + } + + /** + * Get configuration of the plugin. + * + * @return array + */ + public function config() + { + return null !== $this->config ? $this->config["plugins.{$this->name}"] : []; + } + + /** + * Determine if plugin is running under the admin + * + * @return bool + */ + public function isAdmin() + { + return Utils::isAdminPlugin(); + } + + /** + * Determine if plugin is running under the CLI + * + * @return bool + */ + public function isCli() + { + return defined('GRAV_CLI'); + } + + /** + * Determine if this route is in Admin and active for the plugin + * + * @param string $plugin_route + * @return bool + */ + protected function isPluginActiveAdmin($plugin_route) + { + $active = false; + + /** @var Uri $uri */ + $uri = $this->grav['uri']; + /** @var Config $config */ + $config = $this->config ?? $this->grav['config']; + + if (strpos($uri->path(), $config->get('plugins.admin.route') . '/' . $plugin_route) === false) { + $active = false; + } elseif (isset($uri->paths()[1]) && $uri->paths()[1] === $plugin_route) { + $active = true; + } + + return $active; + } + + /** + * @param array $events + * @return void + */ + protected function enable(array $events) + { + /** @var EventDispatcher $dispatcher */ + $dispatcher = $this->grav['events']; + + foreach ($events as $eventName => $params) { + if (is_string($params)) { + $dispatcher->addListener($eventName, [$this, $params]); + } elseif (is_string($params[0])) { + $dispatcher->addListener($eventName, [$this, $params[0]], $this->getPriority($params, $eventName)); + } else { + foreach ($params as $listener) { + $dispatcher->addListener($eventName, [$this, $listener[0]], $this->getPriority($listener, $eventName)); + } + } + } + } + + /** + * @param array $params + * @param string $eventName + * @return int + */ + private function getPriority($params, $eventName) + { + $override = implode('.', ['priorities', $this->name, $eventName, $params[0]]); + + return $this->grav['config']->get($override) ?? $params[1] ?? 0; + } + + /** + * @param array $events + * @return void + */ + protected function disable(array $events) + { + /** @var EventDispatcher $dispatcher */ + $dispatcher = $this->grav['events']; + + foreach ($events as $eventName => $params) { + if (is_string($params)) { + $dispatcher->removeListener($eventName, [$this, $params]); + } elseif (is_string($params[0])) { + $dispatcher->removeListener($eventName, [$this, $params[0]]); + } else { + foreach ($params as $listener) { + $dispatcher->removeListener($eventName, [$this, $listener[0]]); + } + } + } + } + + /** + * Whether or not an offset exists. + * + * @param string $offset An offset to check for. + * @return bool Returns TRUE on success or FALSE on failure. + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + if ($offset === 'title') { + $offset = 'name'; + } + + $blueprint = $this->getBlueprint(); + + return isset($blueprint[$offset]); + } + + /** + * Returns the value at specified offset. + * + * @param string $offset The offset to retrieve. + * @return mixed Can return all value types. + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + if ($offset === 'title') { + $offset = 'name'; + } + + $blueprint = $this->getBlueprint(); + + return $blueprint[$offset] ?? null; + } + + /** + * Assigns a value to the specified offset. + * + * @param string $offset The offset to assign the value to. + * @param mixed $value The value to set. + * @throws LogicException + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + throw new LogicException(__CLASS__ . ' blueprints cannot be modified.'); + } + + /** + * Unsets an offset. + * + * @param string $offset The offset to unset. + * @throws LogicException + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + throw new LogicException(__CLASS__ . ' blueprints cannot be modified.'); + } + + /** + * @return array + */ + public function __debugInfo(): array + { + $array = (array)$this; + + unset($array["\0*\0grav"]); + $array["\0*\0config"] = $this->config(); + + return $array; + } + + /** + * This function will search a string for markdown links in a specific format. The link value can be + * optionally compared against via the $internal_regex and operated on by the callback $function + * provided. + * + * format: [plugin:myplugin_name](function_data) + * + * @param string $content The string to perform operations upon + * @param callable $function The anonymous callback function + * @param string $internal_regex Optional internal regex to extra data from + * @return string + */ + protected function parseLinks($content, $function, $internal_regex = '(.*)') + { + $regex = '/\[plugin:(?:' . preg_quote($this->name, '/') . ')\]\(' . $internal_regex . '\)/i'; + + $result = preg_replace_callback($regex, $function, $content); + \assert($result !== null); + + return $result; + } + + /** + * Merge global and page configurations. + * + * WARNING: This method modifies page header! + * + * @param PageInterface $page The page to merge the configurations with the + * plugin settings. + * @param mixed $deep false = shallow|true = recursive|merge = recursive+unique + * @param array $params Array of additional configuration options to + * merge with the plugin settings. + * @param string $type Is this 'plugins' or 'themes' + * @return Data + */ + protected function mergeConfig(PageInterface $page, $deep = false, $params = [], $type = 'plugins') + { + /** @var Config $config */ + $config = $this->config ?? $this->grav['config']; + + $class_name = $this->name; + $class_name_merged = $class_name . '.merged'; + $defaults = $config->get($type . '.' . $class_name, []); + $page_header = $page->header(); + $header = []; + + if (!isset($page_header->{$class_name_merged}) && isset($page_header->{$class_name})) { + // Get default plugin configurations and retrieve page header configuration + $config = $page_header->{$class_name}; + if (is_bool($config)) { + // Overwrite enabled option with boolean value in page header + $config = ['enabled' => $config]; + } + // Merge page header settings using deep or shallow merging technique + $header = $this->mergeArrays($deep, $defaults, $config); + + // Create new config object and set it on the page object so it's cached for next time + $page->modifyHeader($class_name_merged, new Data($header)); + } elseif (isset($page_header->{$class_name_merged})) { + $merged = $page_header->{$class_name_merged}; + $header = $merged->toArray(); + } + if (empty($header)) { + $header = $defaults; + } + // Merge additional parameter with configuration options + $header = $this->mergeArrays($deep, $header, $params); + + // Return configurations as a new data config class + return new Data($header); + } + + /** + * Merge arrays based on deepness + * + * @param string|bool $deep + * @param array $array1 + * @param array $array2 + * @return array + */ + private function mergeArrays($deep, $array1, $array2) + { + if ($deep === 'merge') { + return Utils::arrayMergeRecursiveUnique($array1, $array2); + } + if ($deep === true) { + return array_replace_recursive($array1, $array2); + } + + return array_merge($array1, $array2); + } + + /** + * Persists to disk the plugin parameters currently stored in the Grav Config object + * + * @param string $name The name of the plugin whose config it should store. + * @return bool + */ + public static function saveConfig($name) + { + if (!$name) { + return false; + } + + $grav = Grav::instance(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + $filename = 'config://plugins/' . $name . '.yaml'; + $file = YamlFile::instance((string)$locator->findResource($filename, true, true)); + $content = $grav['config']->get('plugins.' . $name); + $file->save($content); + $file->free(); + unset($file); + + return true; + } + + /** + * Simpler getter for the plugin blueprint + * + * @return Blueprint + */ + public function getBlueprint() + { + if (null === $this->blueprint) { + $this->loadBlueprint(); + \assert($this->blueprint instanceof Blueprint); + } + + return $this->blueprint; + } + + /** + * Load blueprints. + * + * @return void + */ + protected function loadBlueprint() + { + if (null === $this->blueprint) { + $grav = Grav::instance(); + /** @var Plugins $plugins */ + $plugins = $grav['plugins']; + $data = $plugins->get($this->name); + \assert($data !== null); + $this->blueprint = $data->blueprints(); + } + } +} diff --git a/system/src/Grav/Common/Plugins.php b/system/src/Grav/Common/Plugins.php new file mode 100644 index 0000000..88b5e52 --- /dev/null +++ b/system/src/Grav/Common/Plugins.php @@ -0,0 +1,330 @@ +getIterator('plugins://'); + + $plugins = []; + /** @var SplFileInfo $directory */ + foreach ($iterator as $directory) { + if (!$directory->isDir()) { + continue; + } + $plugins[] = $directory->getFilename(); + } + + sort($plugins, SORT_NATURAL | SORT_FLAG_CASE); + + foreach ($plugins as $plugin) { + $object = $this->loadPlugin($plugin); + if ($object) { + $this->add($object); + } + } + } + + /** + * @return $this + */ + public function setup() + { + $blueprints = []; + $formFields = []; + + $grav = Grav::instance(); + + /** @var Config $config */ + $config = $grav['config']; + + /** @var Plugin $plugin */ + foreach ($this->items as $plugin) { + // Setup only enabled plugins. + if ($config["plugins.{$plugin->name}.enabled"] && $plugin instanceof Plugin) { + if (isset($plugin->features['blueprints'])) { + $blueprints["plugin://{$plugin->name}/blueprints"] = $plugin->features['blueprints']; + } + if (method_exists($plugin, 'getFormFieldTypes')) { + $formFields[get_class($plugin)] = $plugin->features['formfields'] ?? 0; + } + } + } + + if ($blueprints) { + // Order by priority. + arsort($blueprints, SORT_NUMERIC); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + $locator->addPath('blueprints', '', array_keys($blueprints), ['system', 'blueprints']); + } + + if ($formFields) { + // Order by priority. + arsort($formFields, SORT_NUMERIC); + + $list = []; + foreach ($formFields as $className => $priority) { + $plugin = $this->items[$className]; + $list += $plugin->getFormFieldTypes(); + } + + $this->formFieldTypes = $list; + } + + return $this; + } + + /** + * Registers all plugins. + * + * @return Plugin[] array of Plugin objects + * @throws RuntimeException + */ + public function init() + { + if ($this->plugins_initialized) { + return $this->items; + } + + $grav = Grav::instance(); + + /** @var Config $config */ + $config = $grav['config']; + + /** @var EventDispatcher $events */ + $events = $grav['events']; + + foreach ($this->items as $instance) { + // Register only enabled plugins. + if ($config["plugins.{$instance->name}.enabled"] && $instance instanceof Plugin) { + // Set plugin configuration. + $instance->setConfig($config); + // Register autoloader. + if (method_exists($instance, 'autoload')) { + $instance->setAutoloader($instance->autoload()); + } + // Register event listeners. + $events->addSubscriber($instance); + } + } + + // Plugins Loaded Event + $event = new PluginsLoadedEvent($grav, $this); + $grav->dispatchEvent($event); + + $this->plugins_initialized = true; + + return $this->items; + } + + /** + * Add a plugin + * + * @param Plugin $plugin + * @return void + */ + public function add($plugin) + { + if (is_object($plugin)) { + $this->items[get_class($plugin)] = $plugin; + } + } + + /** + * @return array + */ + public function __debugInfo(): array + { + $array = (array)$this; + + unset($array["\0Grav\Common\Iterator\0iteratorUnset"]); + + return $array; + } + + /** + * @return Plugin[] Index of all plugins by plugin name. + */ + public static function getPlugins(): array + { + /** @var Plugins $plugins */ + $plugins = Grav::instance()['plugins']; + + $list = []; + foreach ($plugins as $instance) { + $list[$instance->name] = $instance; + } + + return $list; + } + + /** + * @param string $name Plugin name + * @return Plugin|null Plugin object or null if plugin cannot be found. + */ + public static function getPlugin(string $name) + { + $list = static::getPlugins(); + + return $list[$name] ?? null; + } + + /** + * Return list of all plugin data with their blueprints. + * + * @return Data[] + */ + public static function all() + { + $grav = Grav::instance(); + + /** @var Plugins $plugins */ + $plugins = $grav['plugins']; + $list = []; + + foreach ($plugins as $instance) { + $name = $instance->name; + + try { + $result = self::get($name); + } catch (Exception $e) { + $exception = new RuntimeException(sprintf('Plugin %s: %s', $name, $e->getMessage()), $e->getCode(), $e); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addMessage("Plugin {$name} cannot be loaded, please check Exceptions tab", 'error'); + $debugger->addException($exception); + + continue; + } + + if ($result) { + $list[$name] = $result; + } + } + + return $list; + } + + /** + * Get a plugin by name + * + * @param string $name + * @return Data|null + */ + public static function get($name) + { + $blueprints = new Blueprints('plugins://'); + $blueprint = $blueprints->get("{$name}/blueprints"); + + // Load default configuration. + $file = CompiledYamlFile::instance("plugins://{$name}/{$name}" . YAML_EXT); + + // ensure this is a valid plugin + if (!$file->exists()) { + return null; + } + + $obj = new Data((array)$file->content(), $blueprint); + + // Override with user configuration. + $obj->merge(Grav::instance()['config']->get('plugins.' . $name) ?: []); + + // Save configuration always to user/config. + $file = CompiledYamlFile::instance("config://plugins/{$name}.yaml"); + $obj->file($file); + + return $obj; + } + + /** + * @param string $name + * @return Plugin|null + */ + protected function loadPlugin($name) + { + // NOTE: ALL THE LOCAL VARIABLES ARE USED INSIDE INCLUDED FILE, DO NOT REMOVE THEM! + $grav = Grav::instance(); + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + $class = null; + + // Start by attempting to load the plugin_name.php file. + $file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT); + if (is_file($file)) { + // Local variables available in the file: $grav, $name, $file + $class = include_once $file; + if (!is_object($class) || !is_subclass_of($class, Plugin::class, true)) { + $class = null; + } + } + + // If the class hasn't been initialized yet, guess the class name and create a new instance. + if (null === $class) { + $className = Inflector::camelize($name); + $pluginClassFormat = [ + 'Grav\\Plugin\\' . ucfirst($name). 'Plugin', + 'Grav\\Plugin\\' . $className . 'Plugin', + 'Grav\\Plugin\\' . $className + ]; + + foreach ($pluginClassFormat as $pluginClass) { + if (is_subclass_of($pluginClass, Plugin::class, true)) { + $class = new $pluginClass($name, $grav); + break; + } + } + } + + // Log a warning if plugin cannot be found. + if (null === $class) { + $grav['log']->addWarning( + sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clearcache`", $name) + ); + } + + return $class; + } +} diff --git a/system/src/Grav/Common/Processors/AssetsProcessor.php b/system/src/Grav/Common/Processors/AssetsProcessor.php new file mode 100644 index 0000000..66bb5e3 --- /dev/null +++ b/system/src/Grav/Common/Processors/AssetsProcessor.php @@ -0,0 +1,41 @@ +startTimer(); + $this->container['assets']->init(); + $this->container->fireEvent('onAssetsInitialized'); + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/BackupsProcessor.php b/system/src/Grav/Common/Processors/BackupsProcessor.php new file mode 100644 index 0000000..6e960b4 --- /dev/null +++ b/system/src/Grav/Common/Processors/BackupsProcessor.php @@ -0,0 +1,41 @@ +startTimer(); + $backups = $this->container['backups']; + $backups->init(); + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/DebuggerAssetsProcessor.php b/system/src/Grav/Common/Processors/DebuggerAssetsProcessor.php new file mode 100644 index 0000000..de7cafc --- /dev/null +++ b/system/src/Grav/Common/Processors/DebuggerAssetsProcessor.php @@ -0,0 +1,40 @@ +startTimer(); + $this->container['debugger']->addAssets(); + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/Events/RequestHandlerEvent.php b/system/src/Grav/Common/Processors/Events/RequestHandlerEvent.php new file mode 100644 index 0000000..32908ba --- /dev/null +++ b/system/src/Grav/Common/Processors/Events/RequestHandlerEvent.php @@ -0,0 +1,82 @@ +offsetGet('request'); + } + + /** + * @return Route + */ + public function getRoute(): Route + { + return $this->getRequest()->getAttribute('route'); + } + + /** + * @return RequestHandler + */ + public function getHandler(): RequestHandler + { + return $this->offsetGet('handler'); + } + + /** + * @return ResponseInterface|null + */ + public function getResponse(): ?ResponseInterface + { + return $this->offsetGet('response'); + } + + /** + * @param ResponseInterface $response + * @return $this + */ + public function setResponse(ResponseInterface $response): self + { + $this->offsetSet('response', $response); + $this->stopPropagation(); + + return $this; + } + + /** + * @param string $name + * @param MiddlewareInterface $middleware + * @return RequestHandlerEvent + */ + public function addMiddleware(string $name, MiddlewareInterface $middleware): self + { + /** @var RequestHandler $handler */ + $handler = $this['handler']; + $handler->addMiddleware($name, $middleware); + + return $this; + } +} diff --git a/system/src/Grav/Common/Processors/InitializeProcessor.php b/system/src/Grav/Common/Processors/InitializeProcessor.php new file mode 100644 index 0000000..b31530e --- /dev/null +++ b/system/src/Grav/Common/Processors/InitializeProcessor.php @@ -0,0 +1,460 @@ +processCli(); + } + } + + /** + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $this->startTimer('_init', 'Initialize'); + + // Load configuration. + $config = $this->initializeConfig(); + + // Initialize logger. + $this->initializeLogger($config); + + // Initialize error handlers. + $this->initializeErrors(); + + // Initialize debugger. + $debugger = $this->initializeDebugger(); + + // Debugger can return response right away. + $response = $this->handleDebuggerRequest($debugger, $request); + if ($response) { + $this->stopTimer('_init'); + + return $response; + } + + // Initialize output buffering. + $this->initializeOutputBuffering($config); + + // Set timezone, locale. + $this->initializeLocale($config); + + // Load plugins. + $this->initializePlugins(); + + // Load pages. + $this->initializePages($config); + + // Load accounts (decides class to be used). + // TODO: remove in 2.0. + $this->container['accounts']; + + // Initialize session (used by URI, see issue #3269). + $this->initializeSession($config); + + // Initialize URI (uses session, see issue #3269). + $this->initializeUri($config); + + // Grav may return redirect response right away. + $redirectCode = (int)$config->get('system.pages.redirect_trailing_slash', 1); + if ($redirectCode) { + $response = $this->handleRedirectRequest($request, $redirectCode > 300 ? $redirectCode : null); + if ($response) { + $this->stopTimer('_init'); + + return $response; + } + } + + $this->stopTimer('_init'); + + // Wrap call to next handler so that debugger can profile it. + /** @var Response $response */ + $response = $debugger->profile(static function () use ($handler, $request) { + return $handler->handle($request); + }); + + // Log both request and response and return the response. + return $debugger->logRequest($request, $response); + } + + public function processCli(): void + { + // Load configuration. + $config = $this->initializeConfig(); + + // Initialize logger. + $this->initializeLogger($config); + + // Disable debugger. + $this->container['debugger']->enabled(false); + + // Set timezone, locale. + $this->initializeLocale($config); + + // Load plugins. + $this->initializePlugins(); + + // Load pages. + $this->initializePages($config); + + // Initialize URI. + $this->initializeUri($config); + + // Load accounts (decides class to be used). + // TODO: remove in 2.0. + $this->container['accounts']; + } + + /** + * @return Config + */ + protected function initializeConfig(): Config + { + $this->startTimer('_init_config', 'Configuration'); + + // Initialize Configuration + $grav = $this->container; + + /** @var Config $config */ + $config = $grav['config']; + $config->init(); + $grav['plugins']->setup(); + + if (defined('GRAV_SCHEMA') && $config->get('versions') === null) { + $filename = USER_DIR . 'config/versions.yaml'; + if (!is_file($filename)) { + $versions = [ + 'core' => [ + 'grav' => [ + 'version' => GRAV_VERSION, + 'schema' => GRAV_SCHEMA + ] + ] + ]; + $config->set('versions', $versions); + + $file = new YamlFile($filename, new YamlFormatter(['inline' => 4])); + $file->save($versions); + } + } + + // Override configuration using the environment. + $prefix = 'GRAV_CONFIG'; + $env = getenv($prefix); + if ($env) { + $cPrefix = $prefix . '__'; + $aPrefix = $prefix . '_ALIAS__'; + $cLen = strlen($cPrefix); + $aLen = strlen($aPrefix); + + $keys = $aliases = []; + $env = $_ENV + $_SERVER; + foreach ($env as $key => $value) { + if (!str_starts_with($key, $prefix)) { + continue; + } + if (str_starts_with($key, $cPrefix)) { + $key = str_replace('__', '.', substr($key, $cLen)); + $keys[$key] = $value; + } elseif (str_starts_with($key, $aPrefix)) { + $key = substr($key, $aLen); + $aliases[$key] = $value; + } + } + $list = []; + foreach ($keys as $key => $value) { + foreach ($aliases as $alias => $real) { + $key = str_replace($alias, $real, $key); + } + $list[$key] = $value; + $config->set($key, $value); + } + } + + $this->stopTimer('_init_config'); + + return $config; + } + + /** + * @param Config $config + * @return Logger + */ + protected function initializeLogger(Config $config): Logger + { + $this->startTimer('_init_logger', 'Logger'); + + $grav = $this->container; + + // Initialize Logging + /** @var Logger $log */ + $log = $grav['log']; + + if ($config->get('system.log.handler', 'file') === 'syslog') { + $log->popHandler(); + + $facility = $config->get('system.log.syslog.facility', 'local6'); + $logHandler = new SyslogHandler('grav', $facility); + $formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%"); + $logHandler->setFormatter($formatter); + + $log->pushHandler($logHandler); + } + + $this->stopTimer('_init_logger'); + + return $log; + } + + /** + * @return Errors + */ + protected function initializeErrors(): Errors + { + $this->startTimer('_init_errors', 'Error Handlers Reset'); + + $grav = $this->container; + + // Initialize Error Handlers + /** @var Errors $errors */ + $errors = $grav['errors']; + $errors->resetHandlers(); + + $this->stopTimer('_init_errors'); + + return $errors; + } + + /** + * @return Debugger + */ + protected function initializeDebugger(): Debugger + { + $this->startTimer('_init_debugger', 'Init Debugger'); + + $grav = $this->container; + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->init(); + + $this->stopTimer('_init_debugger'); + + return $debugger; + } + + /** + * @param Debugger $debugger + * @param ServerRequestInterface $request + * @return ResponseInterface|null + */ + protected function handleDebuggerRequest(Debugger $debugger, ServerRequestInterface $request): ?ResponseInterface + { + // Clockwork integration. + $clockwork = $debugger->getClockwork(); + if ($clockwork) { + $server = $request->getServerParams(); +// $baseUri = str_replace('\\', '/', dirname(parse_url($server['SCRIPT_NAME'], PHP_URL_PATH))); +// if ($baseUri === '/') { +// $baseUri = ''; +// } + $requestTime = $server['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME; + + $request = $request->withAttribute('request_time', $requestTime); + + // Handle clockwork API calls. + $uri = $request->getUri(); + if (Utils::contains($uri->getPath(), '/__clockwork/')) { + return $debugger->debuggerRequest($request); + } + + $this->container['clockwork'] = $clockwork; + } + + return null; + } + + /** + * @param Config $config + */ + protected function initializeOutputBuffering(Config $config): void + { + $this->startTimer('_init_ob', 'Initialize Output Buffering'); + + // Use output buffering to prevent headers from being sent too early. + ob_start(); + if ($config->get('system.cache.gzip') && !@ob_start('ob_gzhandler')) { + // Enable zip/deflate with a fallback in case of if browser does not support compressing. + ob_start(); + } + + $this->stopTimer('_init_ob'); + } + + /** + * @param Config $config + */ + protected function initializeLocale(Config $config): void + { + $this->startTimer('_init_locale', 'Initialize Locale'); + + // Initialize the timezone. + $timezone = $config->get('system.timezone'); + if ($timezone) { + date_default_timezone_set($timezone); + } + + $grav = $this->container; + $grav->setLocale(); + + $this->stopTimer('_init_locale'); + } + + protected function initializePlugins(): Plugins + { + $this->startTimer('_init_plugins_load', 'Load Plugins'); + + $grav = $this->container; + + /** @var Plugins $plugins */ + $plugins = $grav['plugins']; + $plugins->init(); + + $this->stopTimer('_init_plugins_load'); + + return $plugins; + } + + protected function initializePages(Config $config): Pages + { + $this->startTimer('_init_pages_register', 'Load Pages'); + + $grav = $this->container; + + /** @var Pages $pages */ + $pages = $grav['pages']; + // Upgrading from older Grav versions won't work without checking if the method exists. + if (method_exists($pages, 'register')) { + $pages->register(); + } + + $this->stopTimer('_init_pages_register'); + + return $pages; + } + + + protected function initializeUri(Config $config): void + { + $this->startTimer('_init_uri', 'Initialize URI'); + + $grav = $this->container; + + /** @var Uri $uri */ + $uri = $grav['uri']; + $uri->init(); + + $this->stopTimer('_init_uri'); + } + + protected function handleRedirectRequest(RequestInterface $request, int $code = null): ?ResponseInterface + { + if (!in_array($request->getMethod(), ['GET', 'HEAD'])) { + return null; + } + + // Redirect pages with trailing slash if configured to do so. + $uri = $request->getUri(); + $path = $uri->getPath() ?: '/'; + $root = $this->container['uri']->rootUrl(); + + if ($path !== $root && $path !== $root . '/' && Utils::endsWith($path, '/')) { + // Use permanent redirect for SEO reasons. + return $this->container->getRedirectResponse((string)$uri->withPath(rtrim($path, '/')), $code); + } + + return null; + } + + /** + * @param Config $config + */ + protected function initializeSession(Config $config): void + { + // FIXME: Initialize session should happen later after plugins have been loaded. This is a workaround to fix session issues in AWS. + if (isset($this->container['session']) && $config->get('system.session.initialize', true)) { + $this->startTimer('_init_session', 'Start Session'); + + /** @var Session $session */ + $session = $this->container['session']; + + try { + $session->init(); + } catch (SessionException $e) { + $session->init(); + $message = 'Session corruption detected, restarting session...'; + $this->addMessage($message); + $this->container['messages']->add($message, 'error'); + } + + $this->stopTimer('_init_session'); + } + } +} diff --git a/system/src/Grav/Common/Processors/PagesProcessor.php b/system/src/Grav/Common/Processors/PagesProcessor.php new file mode 100644 index 0000000..36f6718 --- /dev/null +++ b/system/src/Grav/Common/Processors/PagesProcessor.php @@ -0,0 +1,114 @@ +startTimer(); + + // Dump Cache state + $this->container['debugger']->addMessage($this->container['cache']->getCacheStatus()); + + $this->container['pages']->init(); + + $route = $this->container['route']; + + $this->container->fireEvent('onPagesInitialized', new Event( + [ + 'pages' => $this->container['pages'], + 'route' => $route, + 'request' => $request + ] + )); + $this->container->fireEvent('onPageInitialized', new Event( + [ + 'page' => $this->container['page'], + 'route' => $route, + 'request' => $request + ] + )); + + /** @var PageInterface $page */ + $page = $this->container['page']; + + if (!$page->routable()) { + $exception = new RequestException($request, 'Page Not Found', 404); + // If no page found, fire event + $event = new Event([ + 'page' => $page, + 'code' => $exception->getCode(), + 'message' => $exception->getMessage(), + 'exception' => $exception, + 'route' => $route, + 'request' => $request + ]); + $event->page = null; + $event = $this->container->fireEvent('onPageNotFound', $event); + + if (isset($event->page)) { + unset($this->container['page']); + $this->container['page'] = $page = $event->page; + } else { + throw new RuntimeException('Page Not Found', 404); + } + + $this->addMessage("Routed to page {$page->rawRoute()} (type: {$page->template()}) [Not Found fallback]"); + } else { + $this->addMessage("Routed to page {$page->rawRoute()} (type: {$page->template()})"); + + $task = $this->container['task']; + $action = $this->container['action']; + + /** @var Forms $forms */ + $forms = $this->container['forms'] ?? null; + $form = $forms ? $forms->getActiveForm() : null; + + $options = ['page' => $page, 'form' => $form, 'request' => $request]; + if ($task) { + $event = new Event(['task' => $task] + $options); + $this->container->fireEvent('onPageTask', $event); + $this->container->fireEvent('onPageTask.' . $task, $event); + } elseif ($action) { + $event = new Event(['action' => $action] + $options); + $this->container->fireEvent('onPageAction', $event); + $this->container->fireEvent('onPageAction.' . $action, $event); + } + } + + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/PluginsProcessor.php b/system/src/Grav/Common/Processors/PluginsProcessor.php new file mode 100644 index 0000000..485578e --- /dev/null +++ b/system/src/Grav/Common/Processors/PluginsProcessor.php @@ -0,0 +1,41 @@ +startTimer(); + $grav = $this->container; + $grav->fireEvent('onPluginsInitialized'); + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/ProcessorBase.php b/system/src/Grav/Common/Processors/ProcessorBase.php new file mode 100644 index 0000000..a3506f5 --- /dev/null +++ b/system/src/Grav/Common/Processors/ProcessorBase.php @@ -0,0 +1,70 @@ +container = $container; + } + + /** + * @param string|null $id + * @param string|null $title + */ + protected function startTimer($id = null, $title = null): void + { + /** @var Debugger $debugger */ + $debugger = $this->container['debugger']; + $debugger->startTimer($id ?? $this->id, $title ?? $this->title); + } + + /** + * @param string|null $id + */ + protected function stopTimer($id = null): void + { + /** @var Debugger $debugger */ + $debugger = $this->container['debugger']; + $debugger->stopTimer($id ?? $this->id); + } + + /** + * @param string $message + * @param string $label + * @param bool $isString + */ + protected function addMessage($message, $label = 'info', $isString = true): void + { + /** @var Debugger $debugger */ + $debugger = $this->container['debugger']; + $debugger->addMessage($message, $label, $isString); + } +} diff --git a/system/src/Grav/Common/Processors/ProcessorInterface.php b/system/src/Grav/Common/Processors/ProcessorInterface.php new file mode 100644 index 0000000..8a6edbe --- /dev/null +++ b/system/src/Grav/Common/Processors/ProcessorInterface.php @@ -0,0 +1,20 @@ +startTimer(); + + $container = $this->container; + $output = $container['output']; + + if ($output instanceof ResponseInterface) { + return $output; + } + + /** @var PageInterface $page */ + $page = $this->container['page']; + + // Use internal Grav output. + $container->output = $output; + + ob_start(); + + $event = new Event(['page' => $page, 'output' => &$container->output]); + $container->fireEvent('onOutputGenerated', $event); + + echo $container->output; + + $html = ob_get_clean(); + + // remove any output + $container->output = ''; + + $event = new Event(['page' => $page, 'output' => $html]); + $this->container->fireEvent('onOutputRendered', $event); + + $this->stopTimer(); + + return new Response($page->httpResponseCode(), $page->httpHeaders(), $html); + } +} diff --git a/system/src/Grav/Common/Processors/RequestProcessor.php b/system/src/Grav/Common/Processors/RequestProcessor.php new file mode 100644 index 0000000..971fb67 --- /dev/null +++ b/system/src/Grav/Common/Processors/RequestProcessor.php @@ -0,0 +1,65 @@ +startTimer(); + + $header = $request->getHeaderLine('Content-Type'); + $type = trim(strstr($header, ';', true) ?: $header); + if ($type === 'application/json') { + $request = $request->withParsedBody(json_decode($request->getBody()->getContents(), true)); + } + + $uri = $request->getUri(); + $ext = mb_strtolower(pathinfo($uri->getPath(), PATHINFO_EXTENSION)); + + $request = $request + ->withAttribute('grav', $this->container) + ->withAttribute('time', $_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME) + ->withAttribute('route', Uri::getCurrentRoute()->withExtension($ext)) + ->withAttribute('referrer', $this->container['uri']->referrer()); + + $event = new RequestHandlerEvent(['request' => $request, 'handler' => $handler]); + /** @var RequestHandlerEvent $event */ + $event = $this->container->fireEvent('onRequestHandlerInit', $event); + $response = $event->getResponse(); + $this->stopTimer(); + + if ($response) { + return $response; + } + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/SchedulerProcessor.php b/system/src/Grav/Common/Processors/SchedulerProcessor.php new file mode 100644 index 0000000..69cc163 --- /dev/null +++ b/system/src/Grav/Common/Processors/SchedulerProcessor.php @@ -0,0 +1,42 @@ +startTimer(); + $scheduler = $this->container['scheduler']; + $this->container->fireEvent('onSchedulerInitialized', new Event(['scheduler' => $scheduler])); + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/TasksProcessor.php b/system/src/Grav/Common/Processors/TasksProcessor.php new file mode 100644 index 0000000..07e0934 --- /dev/null +++ b/system/src/Grav/Common/Processors/TasksProcessor.php @@ -0,0 +1,71 @@ +startTimer(); + + $task = $this->container['task']; + $action = $this->container['action']; + if ($task || $action) { + $attributes = $request->getAttribute('controller'); + + $controllerClass = $attributes['class'] ?? null; + if ($controllerClass) { + /** @var RequestHandlerInterface $controller */ + $controller = new $controllerClass($attributes['path'] ?? '', $attributes['params'] ?? []); + try { + $response = $controller->handle($request); + + if ($response->getStatusCode() === 418) { + $response = $handler->handle($request); + } + + $this->stopTimer(); + + return $response; + } catch (NotFoundException $e) { + // Task not found: Let it pass through. + } + } + + if ($task) { + $this->container->fireEvent('onTask.' . $task); + } elseif ($action) { + $this->container->fireEvent('onAction.' . $action); + } + } + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/ThemesProcessor.php b/system/src/Grav/Common/Processors/ThemesProcessor.php new file mode 100644 index 0000000..951dc79 --- /dev/null +++ b/system/src/Grav/Common/Processors/ThemesProcessor.php @@ -0,0 +1,40 @@ +startTimer(); + $this->container['themes']->init(); + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/TwigProcessor.php b/system/src/Grav/Common/Processors/TwigProcessor.php new file mode 100644 index 0000000..6604b5c --- /dev/null +++ b/system/src/Grav/Common/Processors/TwigProcessor.php @@ -0,0 +1,40 @@ +startTimer(); + $this->container['twig']->init(); + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Scheduler/Cron.php b/system/src/Grav/Common/Scheduler/Cron.php new file mode 100644 index 0000000..5127a99 --- /dev/null +++ b/system/src/Grav/Common/Scheduler/Cron.php @@ -0,0 +1,577 @@ + modified for Grav integration + * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved. + * @license MIT License; see LICENSE file for details. + */ + +namespace Grav\Common\Scheduler; + +/* + * Usage examples : + * ---------------- + * + * $cron = new Cron('10-30/5 12 * * *'); + * + * var_dump($cron->getMinutes()); + * // array(5) { + * // [0]=> int(10) + * // [1]=> int(15) + * // [2]=> int(20) + * // [3]=> int(25) + * // [4]=> int(30) + * // } + * + * var_dump($cron->getText('fr')); + * // string(32) "Chaque jour à 12:10,15,20,25,30" + * + * var_dump($cron->getText('en')); + * // string(30) "Every day at 12:10,15,20,25,30" + * + * var_dump($cron->getType()); + * // string(3) "day" + * + * var_dump($cron->getCronHours()); + * // string(2) "12" + * + * var_dump($cron->matchExact(new \DateTime('2012-07-01 13:25:10'))); + * // bool(false) + * + * var_dump($cron->matchExact(new \DateTime('2012-07-01 12:15:20'))); + * // bool(true) + * + * var_dump($cron->matchWithMargin(new \DateTime('2012-07-01 12:32:50'), -3, 5)); + * // bool(true) + */ + +use DateInterval; +use DateTime; +use RuntimeException; +use function count; +use function in_array; +use function is_array; +use function is_string; + +class Cron +{ + public const TYPE_UNDEFINED = ''; + public const TYPE_MINUTE = 'minute'; + public const TYPE_HOUR = 'hour'; + public const TYPE_DAY = 'day'; + public const TYPE_WEEK = 'week'; + public const TYPE_MONTH = 'month'; + public const TYPE_YEAR = 'year'; + /** + * + * @var array + */ + protected $texts = [ + 'fr' => [ + 'empty' => '-tout-', + 'name_minute' => 'minute', + 'name_hour' => 'heure', + 'name_day' => 'jour', + 'name_week' => 'semaine', + 'name_month' => 'mois', + 'name_year' => 'année', + 'text_period' => 'Chaque %s', + 'text_mins' => 'à %s minutes', + 'text_time' => 'à %02s:%02s', + 'text_dow' => 'le %s', + 'text_month' => 'de %s', + 'text_dom' => 'le %s', + 'weekdays' => ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'], + 'months' => ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'], + ], + 'en' => [ + 'empty' => '-all-', + 'name_minute' => 'minute', + 'name_hour' => 'hour', + 'name_day' => 'day', + 'name_week' => 'week', + 'name_month' => 'month', + 'name_year' => 'year', + 'text_period' => 'Every %s', + 'text_mins' => 'at %s minutes past the hour', + 'text_time' => 'at %02s:%02s', + 'text_dow' => 'on %s', + 'text_month' => 'of %s', + 'text_dom' => 'on the %s', + 'weekdays' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'], + 'months' => ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'], + ], + ]; + + /** + * min hour dom month dow + * @var string + */ + protected $cron = ''; + /** + * + * @var array + */ + protected $minutes = []; + /** + * + * @var array + */ + protected $hours = []; + /** + * + * @var array + */ + protected $months = []; + /** + * 0-7 : sunday, monday, ... saturday, sunday + * @var array + */ + protected $dow = []; + /** + * + * @var array + */ + protected $dom = []; + + /** + * @param string|null $cron + */ + public function __construct($cron = null) + { + if (null !== $cron) { + $this->setCron($cron); + } + } + + /** + * @return string + */ + public function getCron() + { + return implode(' ', [ + $this->getCronMinutes(), + $this->getCronHours(), + $this->getCronDaysOfMonth(), + $this->getCronMonths(), + $this->getCronDaysOfWeek(), + ]); + } + + /** + * @param string $lang 'fr' or 'en' + * @return string + */ + public function getText($lang) + { + // check lang + if (!isset($this->texts[$lang])) { + return $this->getCron(); + } + + $texts = $this->texts[$lang]; + // check type + + $type = $this->getType(); + if ($type === self::TYPE_UNDEFINED) { + return $this->getCron(); + } + + // init + $elements = []; + $elements[] = sprintf($texts['text_period'], $texts['name_' . $type]); + + // hour + if ($type === self::TYPE_HOUR) { + $elements[] = sprintf($texts['text_mins'], $this->getCronMinutes()); + } + + // week + if ($type === self::TYPE_WEEK) { + $dow = $this->getCronDaysOfWeek(); + foreach ($texts['weekdays'] as $i => $wd) { + $dow = str_replace((string) ($i + 1), $wd, $dow); + } + $elements[] = sprintf($texts['text_dow'], $dow); + } + + // month + year + if (in_array($type, [self::TYPE_MONTH, self::TYPE_YEAR], true)) { + $elements[] = sprintf($texts['text_dom'], $this->getCronDaysOfMonth()); + } + + // year + if ($type === self::TYPE_YEAR) { + $months = $this->getCronMonths(); + for ($i = count($texts['months']) - 1; $i >= 0; $i--) { + $months = str_replace((string) ($i + 1), $texts['months'][$i], $months); + } + $elements[] = sprintf($texts['text_month'], $months); + } + + // day + week + month + year + if (in_array($type, [self::TYPE_DAY, self::TYPE_WEEK, self::TYPE_MONTH, self::TYPE_YEAR], true)) { + $elements[] = sprintf($texts['text_time'], $this->getCronHours(), $this->getCronMinutes()); + } + + return str_replace('*', $texts['empty'], implode(' ', $elements)); + } + + /** + * @return string + */ + public function getType() + { + $mask = preg_replace('/[^\* ]/', '-', $this->getCron()); + $mask = preg_replace('/-+/', '-', $mask); + $mask = preg_replace('/[^-\*]/', '', $mask); + + if ($mask === '*****') { + return self::TYPE_MINUTE; + } + + if ($mask === '-****') { + return self::TYPE_HOUR; + } + + if (substr($mask, -3) === '***') { + return self::TYPE_DAY; + } + + if (substr($mask, -3) === '-**') { + return self::TYPE_MONTH; + } + + if (substr($mask, -3) === '**-') { + return self::TYPE_WEEK; + } + + if (substr($mask, -2) === '-*') { + return self::TYPE_YEAR; + } + + return self::TYPE_UNDEFINED; + } + + /** + * @param string $cron + * @return $this + */ + public function setCron($cron) + { + // sanitize + $cron = trim($cron); + $cron = preg_replace('/\s+/', ' ', $cron); + // explode + $elements = explode(' ', $cron); + if (count($elements) !== 5) { + throw new RuntimeException('Bad number of elements'); + } + + $this->cron = $cron; + $this->setMinutes($elements[0]); + $this->setHours($elements[1]); + $this->setDaysOfMonth($elements[2]); + $this->setMonths($elements[3]); + $this->setDaysOfWeek($elements[4]); + + return $this; + } + + /** + * @return string + */ + public function getCronMinutes() + { + return $this->arrayToCron($this->minutes); + } + + /** + * @return string + */ + public function getCronHours() + { + return $this->arrayToCron($this->hours); + } + + /** + * @return string + */ + public function getCronDaysOfMonth() + { + return $this->arrayToCron($this->dom); + } + + /** + * @return string + */ + public function getCronMonths() + { + return $this->arrayToCron($this->months); + } + + /** + * @return string + */ + public function getCronDaysOfWeek() + { + return $this->arrayToCron($this->dow); + } + + /** + * @return array + */ + public function getMinutes() + { + return $this->minutes; + } + + /** + * @return array + */ + public function getHours() + { + return $this->hours; + } + + /** + * @return array + */ + public function getDaysOfMonth() + { + return $this->dom; + } + + /** + * @return array + */ + public function getMonths() + { + return $this->months; + } + + /** + * @return array + */ + public function getDaysOfWeek() + { + return $this->dow; + } + + /** + * @param string|string[] $minutes + * @return $this + */ + public function setMinutes($minutes) + { + $this->minutes = $this->cronToArray($minutes, 0, 59); + + return $this; + } + + /** + * @param string|string[] $hours + * @return $this + */ + public function setHours($hours) + { + $this->hours = $this->cronToArray($hours, 0, 23); + + return $this; + } + + /** + * @param string|string[] $months + * @return $this + */ + public function setMonths($months) + { + $this->months = $this->cronToArray($months, 1, 12); + + return $this; + } + + /** + * @param string|string[] $dow + * @return $this + */ + public function setDaysOfWeek($dow) + { + $this->dow = $this->cronToArray($dow, 0, 7); + + return $this; + } + + /** + * @param string|string[] $dom + * @return $this + */ + public function setDaysOfMonth($dom) + { + $this->dom = $this->cronToArray($dom, 1, 31); + + return $this; + } + + /** + * @param mixed $date + * @param int $min + * @param int $hour + * @param int $day + * @param int $month + * @param int $weekday + * @return DateTime + */ + protected function parseDate($date, &$min, &$hour, &$day, &$month, &$weekday) + { + if (is_numeric($date) && (int)$date == $date) { + $date = new DateTime('@' . $date); + } elseif (is_string($date)) { + $date = new DateTime('@' . strtotime($date)); + } + if ($date instanceof DateTime) { + $min = (int)$date->format('i'); + $hour = (int)$date->format('H'); + $day = (int)$date->format('d'); + $month = (int)$date->format('m'); + $weekday = (int)$date->format('w'); // 0-6 + } else { + throw new RuntimeException('Date format not supported'); + } + + return new DateTime($date->format('Y-m-d H:i:sP')); + } + + /** + * @param int|string|DateTime $date + */ + public function matchExact($date) + { + $date = $this->parseDate($date, $min, $hour, $day, $month, $weekday); + + return + (empty($this->minutes) || in_array($min, $this->minutes, true)) && + (empty($this->hours) || in_array($hour, $this->hours, true)) && + (empty($this->dom) || in_array($day, $this->dom, true)) && + (empty($this->months) || in_array($month, $this->months, true)) && + (empty($this->dow) || in_array($weekday, $this->dow, true) || ($weekday == 0 && in_array(7, $this->dow, true)) || ($weekday == 7 && in_array(0, $this->dow, true)) + ); + } + + /** + * @param int|string|DateTime $date + * @param int $minuteBefore + * @param int $minuteAfter + */ + public function matchWithMargin($date, $minuteBefore = 0, $minuteAfter = 0) + { + if ($minuteBefore > 0) { + throw new RuntimeException('MinuteBefore parameter cannot be positive !'); + } + if ($minuteAfter < 0) { + throw new RuntimeException('MinuteAfter parameter cannot be negative !'); + } + + $date = $this->parseDate($date, $min, $hour, $day, $month, $weekday); + $interval = new DateInterval('PT1M'); // 1 min + if ($minuteBefore !== 0) { + $date->sub(new DateInterval('PT' . abs($minuteBefore) . 'M')); + } + $n = $minuteAfter - $minuteBefore + 1; + for ($i = 0; $i < $n; $i++) { + if ($this->matchExact($date)) { + return true; + } + $date->add($interval); + } + + return false; + } + + /** + * @param array $array + * @return string + */ + protected function arrayToCron($array) + { + $n = count($array); + if (!is_array($array) || $n === 0) { + return '*'; + } + + $cron = [$array[0]]; + $s = $c = $array[0]; + for ($i = 1; $i < $n; $i++) { + if ($array[$i] == $c + 1) { + $c = $array[$i]; + $cron[count($cron) - 1] = $s . '-' . $c; + } else { + $s = $c = $array[$i]; + $cron[] = $c; + } + } + + return implode(',', $cron); + } + + /** + * + * @param array|string $string + * @param int $min + * @param int $max + * @return array + */ + protected function cronToArray($string, $min, $max) + { + $array = []; + if (is_array($string)) { + foreach ($string as $val) { + if (is_numeric($val) && (int)$val == $val && $val >= $min && $val <= $max) { + $array[] = (int)$val; + } + } + } elseif ($string !== '*') { + while ($string !== '') { + // test "*/n" expression + if (preg_match('/^\*\/([0-9]+),?/', $string, $m)) { + for ($i = max(0, $min); $i <= min(59, $max); $i += $m[1]) { + $array[] = (int)$i; + } + $string = substr($string, strlen($m[0])); + continue; + } + // test "a-b/n" expression + if (preg_match('/^([0-9]+)-([0-9]+)\/([0-9]+),?/', $string, $m)) { + for ($i = max($m[1], $min); $i <= min($m[2], $max); $i += $m[3]) { + $array[] = (int)$i; + } + $string = substr($string, strlen($m[0])); + continue; + } + // test "a-b" expression + if (preg_match('/^([0-9]+)-([0-9]+),?/', $string, $m)) { + for ($i = max($m[1], $min); $i <= min($m[2], $max); $i++) { + $array[] = (int)$i; + } + $string = substr($string, strlen($m[0])); + continue; + } + // test "c" expression + if (preg_match('/^([0-9]+),?/', $string, $m)) { + if ($m[1] >= $min && $m[1] <= $max) { + $array[] = (int)$m[1]; + } + $string = substr($string, strlen($m[0])); + continue; + } + + // something goes wrong in the expression + return []; + } + } + sort($array, SORT_NUMERIC); + + return $array; + } +} diff --git a/system/src/Grav/Common/Scheduler/IntervalTrait.php b/system/src/Grav/Common/Scheduler/IntervalTrait.php new file mode 100644 index 0000000..cc5c165 --- /dev/null +++ b/system/src/Grav/Common/Scheduler/IntervalTrait.php @@ -0,0 +1,404 @@ +at = $expression; + $this->executionTime = CronExpression::factory($expression); + + return $this; + } + + /** + * Set the execution time to every minute. + * + * @return self + */ + public function everyMinute() + { + return $this->at('* * * * *'); + } + + /** + * Set the execution time to every hour. + * + * @param int|string $minute + * @return self + */ + public function hourly($minute = 0) + { + $c = $this->validateCronSequence($minute); + + return $this->at("{$c['minute']} * * * *"); + } + + /** + * Set the execution time to once a day. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function daily($hour = 0, $minute = 0) + { + if (is_string($hour)) { + $parts = explode(':', $hour); + $hour = $parts[0]; + $minute = $parts[1] ?? '0'; + } + $c = $this->validateCronSequence($minute, $hour); + + return $this->at("{$c['minute']} {$c['hour']} * * *"); + } + + /** + * Set the execution time to once a week. + * + * @param int|string $weekday + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function weekly($weekday = 0, $hour = 0, $minute = 0) + { + if (is_string($hour)) { + $parts = explode(':', $hour); + $hour = $parts[0]; + $minute = $parts[1] ?? '0'; + } + $c = $this->validateCronSequence($minute, $hour, null, null, $weekday); + + return $this->at("{$c['minute']} {$c['hour']} * * {$c['weekday']}"); + } + + /** + * Set the execution time to once a month. + * + * @param int|string $month + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function monthly($month = '*', $day = 1, $hour = 0, $minute = 0) + { + if (is_string($hour)) { + $parts = explode(':', $hour); + $hour = $parts[0]; + $minute = $parts[1] ?? '0'; + } + $c = $this->validateCronSequence($minute, $hour, $day, $month); + + return $this->at("{$c['minute']} {$c['hour']} {$c['day']} {$c['month']} *"); + } + + /** + * Set the execution time to every Sunday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function sunday($hour = 0, $minute = 0) + { + return $this->weekly(0, $hour, $minute); + } + + /** + * Set the execution time to every Monday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function monday($hour = 0, $minute = 0) + { + return $this->weekly(1, $hour, $minute); + } + + /** + * Set the execution time to every Tuesday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function tuesday($hour = 0, $minute = 0) + { + return $this->weekly(2, $hour, $minute); + } + + /** + * Set the execution time to every Wednesday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function wednesday($hour = 0, $minute = 0) + { + return $this->weekly(3, $hour, $minute); + } + + /** + * Set the execution time to every Thursday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function thursday($hour = 0, $minute = 0) + { + return $this->weekly(4, $hour, $minute); + } + + /** + * Set the execution time to every Friday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function friday($hour = 0, $minute = 0) + { + return $this->weekly(5, $hour, $minute); + } + + /** + * Set the execution time to every Saturday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function saturday($hour = 0, $minute = 0) + { + return $this->weekly(6, $hour, $minute); + } + + /** + * Set the execution time to every January. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function january($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(1, $day, $hour, $minute); + } + + /** + * Set the execution time to every February. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function february($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(2, $day, $hour, $minute); + } + + /** + * Set the execution time to every March. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function march($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(3, $day, $hour, $minute); + } + + /** + * Set the execution time to every April. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function april($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(4, $day, $hour, $minute); + } + + /** + * Set the execution time to every May. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function may($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(5, $day, $hour, $minute); + } + + /** + * Set the execution time to every June. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function june($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(6, $day, $hour, $minute); + } + + /** + * Set the execution time to every July. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function july($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(7, $day, $hour, $minute); + } + + /** + * Set the execution time to every August. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function august($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(8, $day, $hour, $minute); + } + + /** + * Set the execution time to every September. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function september($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(9, $day, $hour, $minute); + } + + /** + * Set the execution time to every October. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function october($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(10, $day, $hour, $minute); + } + + /** + * Set the execution time to every November. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function november($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(11, $day, $hour, $minute); + } + + /** + * Set the execution time to every December. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function december($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(12, $day, $hour, $minute); + } + + /** + * Validate sequence of cron expression. + * + * @param int|string|null $minute + * @param int|string|null $hour + * @param int|string|null $day + * @param int|string|null $month + * @param int|string|null $weekday + * @return array + */ + private function validateCronSequence($minute = null, $hour = null, $day = null, $month = null, $weekday = null) + { + return [ + 'minute' => $this->validateCronRange($minute, 0, 59), + 'hour' => $this->validateCronRange($hour, 0, 23), + 'day' => $this->validateCronRange($day, 1, 31), + 'month' => $this->validateCronRange($month, 1, 12), + 'weekday' => $this->validateCronRange($weekday, 0, 6), + ]; + } + + /** + * Validate sequence of cron expression. + * + * @param int|string|null $value + * @param int $min + * @param int $max + * @return mixed + */ + private function validateCronRange($value, $min, $max) + { + if ($value === null || $value === '*') { + return '*'; + } + + if (! is_numeric($value) || + ! ($value >= $min && $value <= $max) + ) { + throw new InvalidArgumentException( + "Invalid value: it should be '*' or between {$min} and {$max}." + ); + } + + return $value; + } +} diff --git a/system/src/Grav/Common/Scheduler/Job.php b/system/src/Grav/Common/Scheduler/Job.php new file mode 100644 index 0000000..13059a5 --- /dev/null +++ b/system/src/Grav/Common/Scheduler/Job.php @@ -0,0 +1,566 @@ +id = Grav::instance()['inflector']->hyphenize($id); + } else { + if (is_string($command)) { + $this->id = md5($command); + } else { + /* @var object $command */ + $this->id = spl_object_hash($command); + } + } + $this->creationTime = new DateTime('now'); + // initialize the directory path for lock files + $this->tempDir = sys_get_temp_dir(); + $this->command = $command; + $this->args = $args; + // Set enabled state + $status = Grav::instance()['config']->get('scheduler.status'); + $this->enabled = !(isset($status[$id]) && $status[$id] === 'disabled'); + } + + /** + * Get the command + * + * @return Closure|string + */ + public function getCommand() + { + return $this->command; + } + + /** + * Get the cron 'at' syntax for this job + * + * @return string + */ + public function getAt() + { + return $this->at; + } + + /** + * Get the status of this job + * + * @return bool + */ + public function getEnabled() + { + return $this->enabled; + } + + /** + * Get optional arguments + * + * @return string|null + */ + public function getArguments() + { + if (is_string($this->args)) { + return $this->args; + } + + return null; + } + + /** + * @return CronExpression + */ + public function getCronExpression() + { + return CronExpression::factory($this->at); + } + + /** + * Get the status of the last run for this job + * + * @return bool + */ + public function isSuccessful() + { + return $this->successful; + } + + /** + * Get the Job id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Check if the Job is due to run. + * It accepts as input a DateTime used to check if + * the job is due. Defaults to job creation time. + * It also default the execution time if not previously defined. + * + * @param DateTime|null $date + * @return bool + */ + public function isDue(DateTime $date = null) + { + // The execution time is being defaulted if not defined + if (!$this->executionTime) { + $this->at('* * * * *'); + } + + $date = $date ?? $this->creationTime; + + return $this->executionTime->isDue($date); + } + + /** + * Check if the Job is overlapping. + * + * @return bool + */ + public function isOverlapping() + { + return $this->lockFile && + file_exists($this->lockFile) && + call_user_func($this->whenOverlapping, filemtime($this->lockFile)) === false; + } + + /** + * Force the Job to run in foreground. + * + * @return $this + */ + public function inForeground() + { + $this->runInBackground = false; + + return $this; + } + + /** + * Sets/Gets an option backlink + * + * @param string|null $link + * @return string|null + */ + public function backlink($link = null) + { + if ($link) { + $this->backlink = $link; + } + return $this->backlink; + } + + + /** + * Check if the Job can run in background. + * + * @return bool + */ + public function runInBackground() + { + return !(is_callable($this->command) || $this->runInBackground === false); + } + + /** + * This will prevent the Job from overlapping. + * It prevents another instance of the same Job of + * being executed if the previous is still running. + * The job id is used as a filename for the lock file. + * + * @param string|null $tempDir The directory path for the lock files + * @param callable|null $whenOverlapping A callback to ignore job overlapping + * @return self + */ + public function onlyOne($tempDir = null, callable $whenOverlapping = null) + { + if ($tempDir === null || !is_dir($tempDir)) { + $tempDir = $this->tempDir; + } + $this->lockFile = implode('/', [ + trim($tempDir), + trim($this->id) . '.lock', + ]); + if ($whenOverlapping) { + $this->whenOverlapping = $whenOverlapping; + } else { + $this->whenOverlapping = static function () { + return false; + }; + } + + return $this; + } + + /** + * Configure the job. + * + * @param array $config + * @return self + */ + public function configure(array $config = []) + { + // Check if config has defined a tempDir + if (isset($config['tempDir']) && is_dir($config['tempDir'])) { + $this->tempDir = $config['tempDir']; + } + + return $this; + } + + /** + * Truth test to define if the job should run if due. + * + * @param callable $fn + * @return self + */ + public function when(callable $fn) + { + $this->truthTest = $fn(); + + return $this; + } + + /** + * Run the job. + * + * @return bool + */ + public function run() + { + // If the truthTest failed, don't run + if ($this->truthTest !== true) { + return false; + } + + // If overlapping, don't run + if ($this->isOverlapping()) { + return false; + } + + // Write lock file if necessary + $this->createLockFile(); + + // Call before if required + if (is_callable($this->before)) { + call_user_func($this->before); + } + + // If command is callable... + if (is_callable($this->command)) { + $this->output = $this->exec(); + } else { + $args = is_string($this->args) ? explode(' ', $this->args) : $this->args; + $command = array_merge([$this->command], $args); + $process = new Process($command); + + $this->process = $process; + + if ($this->runInBackground()) { + $process->start(); + } else { + $process->run(); + $this->finalize(); + } + } + + return true; + } + + /** + * Finish up processing the job + * + * @return void + */ + public function finalize() + { + $process = $this->process; + + if ($process) { + $process->wait(); + + if ($process->isSuccessful()) { + $this->successful = true; + $this->output = $process->getOutput(); + } else { + $this->successful = false; + $this->output = $process->getErrorOutput(); + } + + $this->postRun(); + + unset($this->process); + } + } + + /** + * Things to run after job has run + * + * @return void + */ + private function postRun() + { + if (count($this->outputTo) > 0) { + foreach ($this->outputTo as $file) { + $output_mode = $this->outputMode === 'append' ? FILE_APPEND | LOCK_EX : LOCK_EX; + $timestamp = (new DateTime('now'))->format('c'); + $output = $timestamp . "\n" . str_pad('', strlen($timestamp), '>') . "\n" . $this->output; + file_put_contents($file, $output, $output_mode); + } + } + + // Send output to email + $this->emailOutput(); + + // Call any callback defined + if (is_callable($this->after)) { + call_user_func($this->after, $this->output, $this->returnCode); + } + + $this->removeLockFile(); + } + + /** + * Create the job lock file. + * + * @param mixed $content + * @return void + */ + private function createLockFile($content = null) + { + if ($this->lockFile) { + if ($content === null || !is_string($content)) { + $content = $this->getId(); + } + file_put_contents($this->lockFile, $content); + } + } + + /** + * Remove the job lock file. + * + * @return void + */ + private function removeLockFile() + { + if ($this->lockFile && file_exists($this->lockFile)) { + unlink($this->lockFile); + } + } + + /** + * Execute a callable job. + * + * @return string + * @throws RuntimeException + */ + private function exec() + { + $return_data = ''; + ob_start(); + try { + $return_data = call_user_func_array($this->command, $this->args); + $this->successful = true; + } catch (RuntimeException $e) { + $return_data = $e->getMessage(); + $this->successful = false; + } + $this->output = ob_get_clean() . (is_string($return_data) ? $return_data : ''); + + $this->postRun(); + + return $this->output; + } + + /** + * Set the file/s where to write the output of the job. + * + * @param string|array $filename + * @param bool $append + * @return self + */ + public function output($filename, $append = false) + { + $this->outputTo = is_array($filename) ? $filename : [$filename]; + $this->outputMode = $append === false ? 'overwrite' : 'append'; + + return $this; + } + + /** + * Get the job output. + * + * @return mixed + */ + public function getOutput() + { + return $this->output; + } + + /** + * Set the emails where the output should be sent to. + * The Job should be set to write output to a file + * for this to work. + * + * @param string|array $email + * @return self + */ + public function email($email) + { + if (!is_string($email) && !is_array($email)) { + throw new InvalidArgumentException('The email can be only string or array'); + } + + $this->emailTo = is_array($email) ? $email : [$email]; + // Force the job to run in foreground + $this->inForeground(); + + return $this; + } + + /** + * Email the output of the job, if any. + * + * @return bool + */ + private function emailOutput() + { + if (!count($this->outputTo) || !count($this->emailTo)) { + return false; + } + + if (is_callable('Grav\Plugin\Email\Utils::sendEmail')) { + $subject ='Grav Scheduled Job [' . $this->getId() . ']'; + $content = "

Output from Job ID: {$this->getId()}

\n

Command: {$this->getCommand()}


\n".$this->getOutput()."\n
"; + $to = $this->emailTo; + + \Grav\Plugin\Email\Utils::sendEmail($subject, $content, $to); + } + + return true; + } + + /** + * Set function to be called before job execution + * Job object is injected as a parameter to callable function. + * + * @param callable $fn + * @return self + */ + public function before(callable $fn) + { + $this->before = $fn; + + return $this; + } + + /** + * Set a function to be called after job execution. + * By default this will force the job to run in foreground + * because the output is injected as a parameter of this + * function, but it could be avoided by passing true as a + * second parameter. The job will run in background if it + * meets all the other criteria. + * + * @param callable $fn + * @param bool $runInBackground + * @return self + */ + public function then(callable $fn, $runInBackground = false) + { + $this->after = $fn; + // Force the job to run in foreground + if ($runInBackground === false) { + $this->inForeground(); + } + + return $this; + } +} diff --git a/system/src/Grav/Common/Scheduler/Scheduler.php b/system/src/Grav/Common/Scheduler/Scheduler.php new file mode 100644 index 0000000..73a0712 --- /dev/null +++ b/system/src/Grav/Common/Scheduler/Scheduler.php @@ -0,0 +1,437 @@ +get('scheduler.defaults', []); + $this->config = $config; + + $this->status_path = Grav::instance()['locator']->findResource('user-data://scheduler', true, true); + if (!file_exists($this->status_path)) { + Folder::create($this->status_path); + } + } + + /** + * Load saved jobs from config/scheduler.yaml file + * + * @return $this + */ + public function loadSavedJobs() + { + $this->saved_jobs = []; + $saved_jobs = (array) Grav::instance()['config']->get('scheduler.custom_jobs', []); + + foreach ($saved_jobs as $id => $j) { + $args = $j['args'] ?? []; + $id = Grav::instance()['inflector']->hyphenize($id); + $job = $this->addCommand($j['command'], $args, $id); + + if (isset($j['at'])) { + $job->at($j['at']); + } + + if (isset($j['output'])) { + $mode = isset($j['output_mode']) && $j['output_mode'] === 'append'; + $job->output($j['output'], $mode); + } + + if (isset($j['email'])) { + $job->email($j['email']); + } + + // store in saved_jobs + $this->saved_jobs[] = $job; + } + + return $this; + } + + /** + * Get the queued jobs as background/foreground + * + * @param bool $all + * @return array + */ + public function getQueuedJobs($all = false) + { + $background = []; + $foreground = []; + foreach ($this->jobs as $job) { + if ($all || $job->getEnabled()) { + if ($job->runInBackground()) { + $background[] = $job; + } else { + $foreground[] = $job; + } + } + } + return [$background, $foreground]; + } + + /** + * Get all jobs if they are disabled or not as one array + * + * @return Job[] + */ + public function getAllJobs() + { + [$background, $foreground] = $this->loadSavedJobs()->getQueuedJobs(true); + + return array_merge($background, $foreground); + } + + /** + * Get a specific Job based on id + * + * @param string $jobid + * @return Job|null + */ + public function getJob($jobid) + { + $all = $this->getAllJobs(); + foreach ($all as $job) { + if ($jobid == $job->getId()) { + return $job; + } + } + return null; + } + + /** + * Queues a PHP function execution. + * + * @param callable $fn The function to execute + * @param array $args Optional arguments to pass to the php script + * @param string|null $id Optional custom identifier + * @return Job + */ + public function addFunction(callable $fn, $args = [], $id = null) + { + $job = new Job($fn, $args, $id); + $this->queueJob($job->configure($this->config)); + + return $job; + } + + /** + * Queue a raw shell command. + * + * @param string $command The command to execute + * @param array $args Optional arguments to pass to the command + * @param string|null $id Optional custom identifier + * @return Job + */ + public function addCommand($command, $args = [], $id = null) + { + $job = new Job($command, $args, $id); + $this->queueJob($job->configure($this->config)); + + return $job; + } + + /** + * Run the scheduler. + * + * @param DateTime|null $runTime Optional, run at specific moment + * @param bool $force force run even if not due + */ + public function run(DateTime $runTime = null, $force = false) + { + $this->loadSavedJobs(); + + [$background, $foreground] = $this->getQueuedJobs(false); + $alljobs = array_merge($background, $foreground); + + if (null === $runTime) { + $runTime = new DateTime('now'); + } + + // Star processing jobs + foreach ($alljobs as $job) { + if ($job->isDue($runTime) || $force) { + $job->run(); + $this->jobs_run[] = $job; + } + } + + // Finish handling any background jobs + foreach ($background as $job) { + $job->finalize(); + } + + // Store states + $this->saveJobStates(); + } + + /** + * Reset all collected data of last run. + * + * Call before run() if you call run() multiple times. + * + * @return $this + */ + public function resetRun() + { + // Reset collected data of last run + $this->executed_jobs = []; + $this->failed_jobs = []; + $this->output_schedule = []; + + return $this; + } + + /** + * Get the scheduler verbose output. + * + * @param string $type Allowed: text, html, array + * @return string|array The return depends on the requested $type + */ + public function getVerboseOutput($type = 'text') + { + switch ($type) { + case 'text': + return implode("\n", $this->output_schedule); + case 'html': + return implode('
', $this->output_schedule); + case 'array': + return $this->output_schedule; + default: + throw new InvalidArgumentException('Invalid output type'); + } + } + + /** + * Remove all queued Jobs. + * + * @return $this + */ + public function clearJobs() + { + $this->jobs = []; + + return $this; + } + + /** + * Helper to get the full Cron command + * + * @return string + */ + public function getCronCommand() + { + $command = $this->getSchedulerCommand(); + + return "(crontab -l; echo \"* * * * * {$command} 1>> /dev/null 2>&1\") | crontab -"; + } + + /** + * @param string|null $php + * @return string + */ + public function getSchedulerCommand($php = null) + { + $phpBinaryFinder = new PhpExecutableFinder(); + $php = $php ?? $phpBinaryFinder->find(); + $command = 'cd ' . str_replace(' ', '\ ', GRAV_ROOT) . ';' . $php . ' bin/grav scheduler'; + + return $command; + } + + /** + * Helper to determine if cron job is setup + * 0 - Crontab Not found + * 1 - Crontab Found + * 2 - Error + * + * @return int + */ + public function isCrontabSetup() + { + $process = new Process(['crontab', '-l']); + $process->run(); + + if ($process->isSuccessful()) { + $output = $process->getOutput(); + $command = str_replace('/', '\/', $this->getSchedulerCommand('.*')); + $full_command = '/^(?!#).* .* .* .* .* ' . $command . '/m'; + + return preg_match($full_command, $output) ? 1 : 0; + } + + $error = $process->getErrorOutput(); + + return Utils::startsWith($error, 'crontab: no crontab') ? 0 : 2; + } + + /** + * Get the Job states file + * + * @return YamlFile + */ + public function getJobStates() + { + return YamlFile::instance($this->status_path . '/status.yaml'); + } + + /** + * Save job states to statys file + * + * @return void + */ + private function saveJobStates() + { + $now = time(); + $new_states = []; + + foreach ($this->jobs_run as $job) { + if ($job->isSuccessful()) { + $new_states[$job->getId()] = ['state' => 'success', 'last-run' => $now]; + $this->pushExecutedJob($job); + } else { + $new_states[$job->getId()] = ['state' => 'failure', 'last-run' => $now, 'error' => $job->getOutput()]; + $this->pushFailedJob($job); + } + } + + $saved_states = $this->getJobStates(); + $saved_states->save(array_merge($saved_states->content(), $new_states)); + } + + /** + * Try to determine who's running the process + * + * @return false|string + */ + public function whoami() + { + $process = new Process('whoami'); + $process->run(); + + if ($process->isSuccessful()) { + return trim($process->getOutput()); + } + + return $process->getErrorOutput(); + } + + + /** + * Queue a job for execution in the correct queue. + * + * @param Job $job + * @return void + */ + private function queueJob(Job $job) + { + $this->jobs[] = $job; + + // Store jobs + } + + /** + * Add an entry to the scheduler verbose output array. + * + * @param string $string + * @return void + */ + private function addSchedulerVerboseOutput($string) + { + $now = '[' . (new DateTime('now'))->format('c') . '] '; + $this->output_schedule[] = $now . $string; + // Print to stdoutput in light gray + // echo "\033[37m{$string}\033[0m\n"; + } + + /** + * Push a succesfully executed job. + * + * @param Job $job + * @return Job + */ + private function pushExecutedJob(Job $job) + { + $this->executed_jobs[] = $job; + $command = $job->getCommand(); + $args = $job->getArguments(); + // If callable, log the string Closure + if (is_callable($command)) { + $command = is_string($command) ? $command : 'Closure'; + } + $this->addSchedulerVerboseOutput("Success: {$command} {$args}"); + + return $job; + } + + /** + * Push a failed job. + * + * @param Job $job + * @return Job + */ + private function pushFailedJob(Job $job) + { + $this->failed_jobs[] = $job; + $command = $job->getCommand(); + // If callable, log the string Closure + if (is_callable($command)) { + $command = is_string($command) ? $command : 'Closure'; + } + $output = trim($job->getOutput()); + $this->addSchedulerVerboseOutput("Error: {$command}{$output}"); + + return $job; + } +} diff --git a/system/src/Grav/Common/Security.php b/system/src/Grav/Common/Security.php new file mode 100644 index 0000000..fe259d8 --- /dev/null +++ b/system/src/Grav/Common/Security.php @@ -0,0 +1,252 @@ +get('security.sanitize_svg')) { + $sanitizer = new DOMSanitizer(DOMSanitizer::SVG); + $sanitized = $sanitizer->sanitize($svg); + if (is_string($sanitized)) { + $svg = $sanitized; + } + } + + return $svg; + } + + /** + * Sanitize SVG for XSS code + * + * @param string $file + * @return void + */ + public static function sanitizeSVG(string $file): void + { + if (file_exists($file) && Grav::instance()['config']->get('security.sanitize_svg')) { + $sanitizer = new DOMSanitizer(DOMSanitizer::SVG); + $original_svg = file_get_contents($file); + $clean_svg = $sanitizer->sanitize($original_svg); + + // Quarantine bad SVG files and throw exception + if ($clean_svg !== false ) { + file_put_contents($file, $clean_svg); + } else { + $quarantine_file = basename($file); + $quarantine_dir = 'log://quarantine'; + Folder::mkdir($quarantine_dir); + file_put_contents("$quarantine_dir/$quarantine_file", $original_svg); + unlink($file); + throw new Exception('SVG could not be sanitized, it has been moved to the logs/quarantine folder'); + } + } + } + + /** + * Detect XSS code in Grav pages + * + * @param Pages $pages + * @param bool $route + * @param callable|null $status + * @return array + */ + public static function detectXssFromPages(Pages $pages, $route = true, callable $status = null) + { + $routes = $pages->routes(); + + // Remove duplicate for homepage + unset($routes['/']); + + $list = []; + + // This needs Symfony 4.1 to work + $status && $status([ + 'type' => 'count', + 'steps' => count($routes), + ]); + + foreach ($routes as $path) { + $status && $status([ + 'type' => 'progress', + ]); + + try { + $page = $pages->get($path); + + // call the content to load/cache it + $header = (array) $page->header(); + $content = $page->value('content'); + + $data = ['header' => $header, 'content' => $content]; + $results = static::detectXssFromArray($data); + + if (!empty($results)) { + if ($route) { + $list[$page->route()] = $results; + } else { + $list[$page->filePathClean()] = $results; + } + } + } catch (Exception $e) { + continue; + } + } + + return $list; + } + + /** + * Detect XSS in an array or strings such as $_POST or $_GET + * + * @param array $array Array such as $_POST or $_GET + * @param array|null $options Extra options to be passed. + * @param string $prefix Prefix for returned values. + * @return array Returns flatten list of potentially dangerous input values, such as 'data.content'. + */ + public static function detectXssFromArray(array $array, string $prefix = '', array $options = null) + { + if (null === $options) { + $options = static::getXssDefaults(); + } + + $list = [[]]; + foreach ($array as $key => $value) { + if (is_array($value)) { + $list[] = static::detectXssFromArray($value, $prefix . $key . '.', $options); + } + if ($result = static::detectXss($value, $options)) { + $list[] = [$prefix . $key => $result]; + } + } + + return array_merge(...$list); + } + + /** + * Determine if string potentially has a XSS attack. This simple function does not catch all XSS and it is likely to + * + * return false positives because of it tags all potentially dangerous HTML tags and attributes without looking into + * their content. + * + * @param string|null $string The string to run XSS detection logic on + * @param array|null $options + * @return string|null Type of XSS vector if the given `$string` may contain XSS, false otherwise. + * + * Copies the code from: https://github.com/symphonycms/xssfilter/blob/master/extension.driver.php#L138 + */ + public static function detectXss($string, array $options = null): ?string + { + // Skip any null or non string values + if (null === $string || !is_string($string) || empty($string)) { + return null; + } + + if (null === $options) { + $options = static::getXssDefaults(); + } + + $enabled_rules = (array)($options['enabled_rules'] ?? null); + $dangerous_tags = (array)($options['dangerous_tags'] ?? null); + if (!$dangerous_tags) { + $enabled_rules['dangerous_tags'] = false; + } + $invalid_protocols = (array)($options['invalid_protocols'] ?? null); + if (!$invalid_protocols) { + $enabled_rules['invalid_protocols'] = false; + } + $enabled_rules = array_filter($enabled_rules, static function ($val) { return !empty($val); }); + if (!$enabled_rules) { + return null; + } + + // Keep a copy of the original string before cleaning up + $orig = $string; + + // URL decode + $string = urldecode($string); + + // Convert Hexadecimals + $string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', static function ($m) { + return chr(hexdec($m[2])); + }, $string); + + // Clean up entities + $string = preg_replace('!(�+[0-9]+)!u', '$1;', $string); + + // Decode entities + $string = html_entity_decode($string, ENT_NOQUOTES | ENT_HTML5, 'UTF-8'); + + // Strip whitespace characters + $string = preg_replace('!\s!u', '', $string); + + // Set the patterns we'll test against + $patterns = [ + // Match any attribute starting with "on" or xmlns + 'on_events' => '#(<[^>]+[[a-z\x00-\x20\"\'\/])([\s\/]on|\sxmlns)[a-z].*=>?#iUu', + + // Match javascript:, livescript:, vbscript:, mocha:, feed: and data: protocols + 'invalid_protocols' => '#(' . implode('|', array_map('preg_quote', $invalid_protocols, ['#'])) . '):\S.*?#iUu', + + // Match -moz-bindings + 'moz_binding' => '#-moz-binding[a-z\x00-\x20]*:#u', + + // Match style attributes + 'html_inline_styles' => '#(<[^>]+[a-z\x00-\x20\"\'\/])(style=[^>]*(url\:|x\:expression).*)>?#iUu', + + // Match potentially dangerous tags + 'dangerous_tags' => '#]*>?#ui' + ]; + + // Iterate over rules and return label if fail + foreach ($patterns as $name => $regex) { + if (!empty($enabled_rules[$name])) { + if (preg_match($regex, $string) || preg_match($regex, $orig)) { + return $name; + } + } + } + + return null; + } + + public static function getXssDefaults(): array + { + /** @var Config $config */ + $config = Grav::instance()['config']; + + return [ + 'enabled_rules' => $config->get('security.xss_enabled'), + 'dangerous_tags' => array_map('trim', $config->get('security.xss_dangerous_tags')), + 'invalid_protocols' => array_map('trim', $config->get('security.xss_invalid_protocols')), + ]; + } +} diff --git a/system/src/Grav/Common/Service/AccountsServiceProvider.php b/system/src/Grav/Common/Service/AccountsServiceProvider.php new file mode 100644 index 0000000..8b158c9 --- /dev/null +++ b/system/src/Grav/Common/Service/AccountsServiceProvider.php @@ -0,0 +1,157 @@ +addTypes($config->get('permissions.types', [])); + + $array = $config->get('permissions.actions'); + if (is_array($array)) { + $actions = PermissionsReader::fromArray($array, $permissions->getTypes()); + $permissions->addActions($actions); + } + + $event = new PermissionsRegisterEvent($permissions); + $container->dispatchEvent($event); + + return $permissions; + }; + + $container['accounts'] = function (Container $container) { + $type = $this->initialize($container); + + return $type === 'flex' ? $this->flexAccounts($container) : $this->regularAccounts($container); + }; + + $container['user_groups'] = static function (Container $container) { + /** @var Flex $flex */ + $flex = $container['flex']; + $directory = $flex->getDirectory('user-groups'); + + return $directory ? $directory->getIndex() : null; + }; + + $container['users'] = $container->factory(static function (Container $container) { + user_error('Grav::instance()[\'users\'] is deprecated since Grav 1.6, use Grav::instance()[\'accounts\'] instead', E_USER_DEPRECATED); + + return $container['accounts']; + }); + } + + /** + * @param Container $container + * @return string + */ + protected function initialize(Container $container): string + { + $isDefined = defined('GRAV_USER_INSTANCE'); + $type = strtolower($isDefined ? GRAV_USER_INSTANCE : $container['config']->get('system.accounts.type', 'regular')); + + if ($type === 'flex') { + if (!$isDefined) { + define('GRAV_USER_INSTANCE', 'FLEX'); + } + + /** @var EventDispatcher $dispatcher */ + $dispatcher = $container['events']; + + // Stop /admin/user from working, display error instead. + $dispatcher->addListener( + 'onAdminPage', + static function (Event $event) { + $grav = Grav::instance(); + $admin = $grav['admin']; + [$base,$location,] = $admin->getRouteDetails(); + if ($location !== 'user' || isset($grav['flex_objects'])) { + return; + } + + /** @var PageInterface $page */ + $page = $event['page']; + $page->init(new SplFileInfo('plugin://admin/pages/admin/error.md')); + $page->routable(true); + $header = $page->header(); + $header->title = 'Please install missing plugin'; + $page->content("## Please install and enable **[Flex Objects]({$base}/plugins/flex-objects)** plugin. It is required to edit **Flex User Accounts**."); + + /** @var Header $header */ + $header = $page->header(); + $directory = $grav['accounts']->getFlexDirectory(); + $menu = $directory->getConfig('admin.menu.list'); + $header->access = $menu['authorize'] ?? ['admin.super']; + }, + 100000 + ); + } elseif (!$isDefined) { + define('GRAV_USER_INSTANCE', 'REGULAR'); + } + + return $type; + } + + /** + * @param Container $container + * @return DataUser\UserCollection + */ + protected function regularAccounts(Container $container) + { + // Use User class for backwards compatibility. + return new DataUser\UserCollection(User::class); + } + + /** + * @param Container $container + * @return FlexIndexInterface|null + */ + protected function flexAccounts(Container $container) + { + /** @var Flex $flex */ + $flex = $container['flex']; + $directory = $flex->getDirectory('user-accounts'); + + return $directory ? $directory->getIndex() : null; + } +} diff --git a/system/src/Grav/Common/Service/AssetsServiceProvider.php b/system/src/Grav/Common/Service/AssetsServiceProvider.php new file mode 100644 index 0000000..1e4e647 --- /dev/null +++ b/system/src/Grav/Common/Service/AssetsServiceProvider.php @@ -0,0 +1,32 @@ +setup(); + + return $backups; + }; + } +} diff --git a/system/src/Grav/Common/Service/ConfigServiceProvider.php b/system/src/Grav/Common/Service/ConfigServiceProvider.php new file mode 100644 index 0000000..3a5c2e2 --- /dev/null +++ b/system/src/Grav/Common/Service/ConfigServiceProvider.php @@ -0,0 +1,206 @@ +init(); + + return $setup; + }; + + $container['blueprints'] = function ($c) { + return static::blueprints($c); + }; + + $container['config'] = function ($c) { + $config = static::load($c); + + // After configuration has been loaded, we can disable YAML compatibility if strict mode has been enabled. + if (!$config->get('system.strict_mode.yaml_compat', true)) { + YamlFile::globalSettings(['compat' => false, 'native' => true]); + } + + return $config; + }; + + $container['mime'] = function ($c) { + /** @var Config $config */ + $config = $c['config']; + $mimes = $config->get('mime.types', []); + foreach ($config->get('media.types', []) as $ext => $media) { + if (!empty($media['mime'])) { + $mimes[$ext] = array_unique(array_merge([$media['mime']], $mimes[$ext] ?? [])); + } + } + + return MimeTypes::createFromMimes($mimes); + }; + + $container['languages'] = function ($c) { + return static::languages($c); + }; + + $container['language'] = function ($c) { + return new Language($c); + }; + } + + /** + * @param Container $container + * @return mixed + */ + public static function blueprints(Container $container) + { + /** Setup $setup */ + $setup = $container['setup']; + + /** @var UniformResourceLocator $locator */ + $locator = $container['locator']; + + $cache = $locator->findResource('cache://compiled/blueprints', true, true); + + $files = []; + $paths = $locator->findResources('blueprints://config'); + $files += (new ConfigFileFinder)->locateFiles($paths); + $paths = $locator->findResources('plugins://'); + $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'blueprints'); + $paths = $locator->findResources('themes://'); + $files += (new ConfigFileFinder)->setBase('themes')->locateInFolders($paths, 'blueprints'); + + $blueprints = new CompiledBlueprints($cache, $files, GRAV_ROOT); + + return $blueprints->name("master-{$setup->environment}")->load(); + } + + /** + * @param Container $container + * @return Config + */ + public static function load(Container $container) + { + /** Setup $setup */ + $setup = $container['setup']; + + /** @var UniformResourceLocator $locator */ + $locator = $container['locator']; + + $cache = $locator->findResource('cache://compiled/config', true, true); + + $files = []; + $paths = $locator->findResources('config://'); + $files += (new ConfigFileFinder)->locateFiles($paths); + $paths = $locator->findResources('plugins://'); + $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths); + $paths = $locator->findResources('themes://'); + $files += (new ConfigFileFinder)->setBase('themes')->locateInFolders($paths); + + $compiled = new CompiledConfig($cache, $files, GRAV_ROOT); + $compiled->setBlueprints(function () use ($container) { + return $container['blueprints']; + }); + + $config = $compiled->name("master-{$setup->environment}")->load(); + $config->environment = $setup->environment; + + return $config; + } + + /** + * @param Container $container + * @return mixed + */ + public static function languages(Container $container) + { + /** @var Setup $setup */ + $setup = $container['setup']; + + /** @var Config $config */ + $config = $container['config']; + + /** @var UniformResourceLocator $locator */ + $locator = $container['locator']; + + $cache = $locator->findResource('cache://compiled/languages', true, true); + $files = []; + + // Process languages only if enabled in configuration. + if ($config->get('system.languages.translations', true)) { + $paths = $locator->findResources('languages://'); + $files += (new ConfigFileFinder)->locateFiles($paths); + $paths = $locator->findResources('plugins://'); + $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'languages'); + $paths = static::pluginFolderPaths($paths, 'languages'); + $files += (new ConfigFileFinder)->locateFiles($paths); + } + + $languages = new CompiledLanguages($cache, $files, GRAV_ROOT); + + return $languages->name("master-{$setup->environment}")->load(); + } + + /** + * Find specific paths in plugins + * + * @param array $plugins + * @param string $folder_path + * @return array + */ + protected static function pluginFolderPaths($plugins, $folder_path) + { + $paths = []; + + foreach ($plugins as $path) { + $iterator = new DirectoryIterator($path); + + /** @var DirectoryIterator $directory */ + foreach ($iterator as $directory) { + if (!$directory->isDir() || $directory->isDot()) { + continue; + } + + // Path to the languages folder + $lang_path = $directory->getPathName() . '/' . $folder_path; + + // If this folder exists, add it to the list of paths + if (file_exists($lang_path)) { + $paths []= $lang_path; + } + } + } + return $paths; + } +} diff --git a/system/src/Grav/Common/Service/ErrorServiceProvider.php b/system/src/Grav/Common/Service/ErrorServiceProvider.php new file mode 100644 index 0000000..3227365 --- /dev/null +++ b/system/src/Grav/Common/Service/ErrorServiceProvider.php @@ -0,0 +1,30 @@ + $config->get('system.flex', [])]); + FlexFormFlash::setFlex($flex); + + $accountsEnabled = $config->get('system.accounts.type', 'regular') === 'flex'; + $pagesEnabled = $config->get('system.pages.type', 'regular') === 'flex'; + + // Add built-in types from Grav. + if ($pagesEnabled) { + $flex->addDirectoryType( + 'pages', + 'blueprints://flex/pages.yaml', + [ + 'enabled' => $pagesEnabled + ] + ); + } + if ($accountsEnabled) { + $flex->addDirectoryType( + 'user-accounts', + 'blueprints://flex/user-accounts.yaml', + [ + 'enabled' => $accountsEnabled, + 'data' => [ + 'storage' => $this->getFlexAccountsStorage($config), + ] + ] + ); + $flex->addDirectoryType( + 'user-groups', + 'blueprints://flex/user-groups.yaml', + [ + 'enabled' => $accountsEnabled + ] + ); + } + + // Call event to register Flex Directories. + $event = new FlexRegisterEvent($flex); + $container->dispatchEvent($event); + + return $flex; + }; + } + + /** + * @param Config $config + * @return array + */ + private function getFlexAccountsStorage(Config $config): array + { + $value = $config->get('system.accounts.storage', 'file'); + if (is_array($value)) { + return $value; + } + + if ($value === 'folder') { + return [ + 'class' => UserFolderStorage::class, + 'options' => [ + 'file' => 'user', + 'pattern' => '{FOLDER}/{KEY:2}/{KEY}/{FILE}{EXT}', + 'key' => 'storage_key' + ], + ]; + } + + if ($value === 'file') { + return [ + 'class' => UserFileStorage::class, + 'options' => [ + 'pattern' => '{FOLDER}/{KEY}{EXT}', + 'key' => 'username' + ], + ]; + } + + return []; + } +} diff --git a/system/src/Grav/Common/Service/InflectorServiceProvider.php b/system/src/Grav/Common/Service/InflectorServiceProvider.php new file mode 100644 index 0000000..861a69e --- /dev/null +++ b/system/src/Grav/Common/Service/InflectorServiceProvider.php @@ -0,0 +1,32 @@ +findResource('log://grav.log', true, true); + $log->pushHandler(new StreamHandler($log_file, Logger::DEBUG)); + + return $log; + }; + } +} diff --git a/system/src/Grav/Common/Service/OutputServiceProvider.php b/system/src/Grav/Common/Service/OutputServiceProvider.php new file mode 100644 index 0000000..9a49185 --- /dev/null +++ b/system/src/Grav/Common/Service/OutputServiceProvider.php @@ -0,0 +1,39 @@ +processSite($page->templateFormat()); + }; + } +} diff --git a/system/src/Grav/Common/Service/PagesServiceProvider.php b/system/src/Grav/Common/Service/PagesServiceProvider.php new file mode 100644 index 0000000..55f6a45 --- /dev/null +++ b/system/src/Grav/Common/Service/PagesServiceProvider.php @@ -0,0 +1,139 @@ +findResource('system://pages/notfound.md'); + $page = new Page(); + $page->init(new SplFileInfo($path)); + $page->routable(false); + + return $page; + }; + + return; + } + + $container['page'] = static function (Grav $grav) { + /** @var Pages $pages */ + $pages = $grav['pages']; + + /** @var Config $config */ + $config = $grav['config']; + + /** @var Uri $uri */ + $uri = $grav['uri']; + + $path = $uri->path() ?: '/'; // Don't trim to support trailing slash default routes + $page = $pages->dispatch($path); + + // Redirection tests + if ($page) { + // some debugger override logic + if ($page->debugger() === false) { + $grav['debugger']->enabled(false); + } + + if ($config->get('system.force_ssl')) { + $scheme = $uri->scheme(true); + if ($scheme !== 'https') { + $url = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + $grav->redirect($url); + } + } + + $route = $page->route(); + if ($route && \in_array($uri->method(), ['GET', 'HEAD'], true)) { + $pageExtension = $page->urlExtension(); + $url = $pages->route($route) . $pageExtension; + + if ($uri->params()) { + if ($url === '/') { //Avoid double slash + $url = $uri->params(); + } else { + $url .= $uri->params(); + } + } + if ($uri->query()) { + $url .= '?' . $uri->query(); + } + if ($uri->fragment()) { + $url .= '#' . $uri->fragment(); + } + + /** @var Language $language */ + $language = $grav['language']; + + $redirectCode = (int)$config->get('system.pages.redirect_default_route', 0); + + // Language-specific redirection scenarios + if ($language->enabled() && ($language->isLanguageInUrl() xor $language->isIncludeDefaultLanguage())) { + $grav->redirect($url, $redirectCode); + } + + // Default route test and redirect + if ($redirectCode) { + $uriExtension = $uri->extension(); + $uriExtension = null !== $uriExtension ? '.' . $uriExtension : ''; + + if ($route !== $path || ($pageExtension !== $uriExtension + && \in_array($pageExtension, ['', '.htm', '.html'], true) + && \in_array($uriExtension, ['', '.htm', '.html'], true))) { + $grav->redirect($url, $redirectCode); + } + } + } + } + + // if page is not found, try some fallback stuff + if (!$page || !$page->routable()) { + // Try fallback URL stuff... + $page = $grav->fallbackUrl($path); + + if (!$page) { + $path = $grav['locator']->findResource('system://pages/notfound.md'); + $page = new Page(); + $page->init(new SplFileInfo($path)); + $page->routable(false); + } + } + + return $page; + }; + } +} diff --git a/system/src/Grav/Common/Service/RequestServiceProvider.php b/system/src/Grav/Common/Service/RequestServiceProvider.php new file mode 100644 index 0000000..17b3149 --- /dev/null +++ b/system/src/Grav/Common/Service/RequestServiceProvider.php @@ -0,0 +1,103 @@ + $headerValue) { + if ('content-type' !== strtolower($headerName)) { + continue; + } + + $contentType = strtolower(trim(explode(';', $headerValue, 2)[0])); + switch ($contentType) { + case 'application/x-www-form-urlencoded': + case 'multipart/form-data': + $post = $_POST; + break 2; + case 'application/json': + case 'application/vnd.api+json': + try { + $json = file_get_contents('php://input'); + $post = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + if (!is_array($post)) { + $post = null; + } + } catch (JsonException $e) { + $post = null; + } + break 2; + } + } + } + + // Remove _url from ngnix routes. + $get = $_GET; + unset($get['_url']); + if (isset($server['QUERY_STRING'])) { + $query = $server['QUERY_STRING']; + if (strpos($query, '_url=') !== false) { + parse_str($query, $query); + unset($query['_url']); + $server['QUERY_STRING'] = http_build_query($query); + } + } + + return $creator->fromArrays($server, $headers, $_COOKIE, $get, $post, $_FILES, fopen('php://input', 'rb') ?: null); + }; + + $container['route'] = $container->factory(function () { + return clone Uri::getCurrentRoute(); + }); + } +} diff --git a/system/src/Grav/Common/Service/SchedulerServiceProvider.php b/system/src/Grav/Common/Service/SchedulerServiceProvider.php new file mode 100644 index 0000000..a952725 --- /dev/null +++ b/system/src/Grav/Common/Service/SchedulerServiceProvider.php @@ -0,0 +1,32 @@ +get('system.session.enabled', false); + $cookie_secure = (bool)$config->get('system.session.secure', false); + $cookie_httponly = (bool)$config->get('system.session.httponly', true); + $cookie_lifetime = (int)$config->get('system.session.timeout', 1800); + $cookie_domain = $config->get('system.session.domain'); + $cookie_path = $config->get('system.session.path'); + $cookie_samesite = $config->get('system.session.samesite', 'Lax'); + + if (null === $cookie_domain) { + $cookie_domain = $uri->host(); + if ($cookie_domain === 'localhost') { + $cookie_domain = ''; + } + } + + if (null === $cookie_path) { + $cookie_path = '/' . trim(Uri::filterPath($uri->rootUrl(false)), '/'); + } + // Session cookie path requires trailing slash. + $cookie_path = rtrim($cookie_path, '/') . '/'; + + // Activate admin if we're inside the admin path. + $is_admin = false; + if ($config->get('plugins.admin.enabled')) { + $admin_base = '/' . trim($config->get('plugins.admin.route'), '/'); + + // Uri::route() is not processed yet, let's quickly get what we need. + $current_route = str_replace(Uri::filterPath($uri->rootUrl(false)), '', parse_url($uri->url(true), PHP_URL_PATH)); + + // Test to see if path starts with a supported language + admin base + $lang = Utils::pathPrefixedByLangCode($current_route); + $lang_admin_base = '/' . $lang . $admin_base; + + // Check no language, simple language prefix (en) and region specific language prefix (en-US). + if (Utils::startsWith($current_route, $admin_base) || Utils::startsWith($current_route, $lang_admin_base)) { + $cookie_lifetime = $config->get('plugins.admin.session.timeout', 1800); + $enabled = $is_admin = true; + } + } + + // Fix for HUGE session timeouts. + if ($cookie_lifetime > 99999999999) { + $cookie_lifetime = 9999999999; + } + + $session_prefix = $c['inflector']->hyphenize($config->get('system.session.name', 'grav-site')); + $session_uniqueness = $config->get('system.session.uniqueness', 'path') === 'path' ? substr(md5(GRAV_ROOT), 0, 7) : md5($config->get('security.salt')); + + $session_name = $session_prefix . '-' . $session_uniqueness; + + if ($is_admin && $config->get('system.session.split', true)) { + $session_name .= '-admin'; + } + + // Define session service. + $options = [ + 'name' => $session_name, + 'cookie_lifetime' => $cookie_lifetime, + 'cookie_path' => $cookie_path, + 'cookie_domain' => $cookie_domain, + 'cookie_secure' => $cookie_secure, + 'cookie_httponly' => $cookie_httponly, + 'cookie_samesite' => $cookie_samesite + ] + (array) $config->get('system.session.options'); + + $session = new Session($options); + $session->setAutoStart($enabled); + + return $session; + }; + + // Define session message service. + $container['messages'] = function ($c) { + if (!isset($c['session']) || !$c['session']->isStarted()) { + /** @var Debugger $debugger */ + $debugger = $c['debugger']; + $debugger->addMessage('Inactive session: session messages may disappear', 'warming'); + + return new Messages(); + } + + /** @var Session $session */ + $session = $c['session']; + + if (!$session->messages instanceof Messages) { + $session->messages = new Messages(); + } + + return $session->messages; + }; + } +} diff --git a/system/src/Grav/Common/Service/StreamsServiceProvider.php b/system/src/Grav/Common/Service/StreamsServiceProvider.php new file mode 100644 index 0000000..edde09a --- /dev/null +++ b/system/src/Grav/Common/Service/StreamsServiceProvider.php @@ -0,0 +1,56 @@ +initializeLocator($locator); + + return $locator; + }; + + $container['streams'] = function (Container $container) { + /** @var Setup $setup */ + $setup = $container['setup']; + + /** @var UniformResourceLocator $locator */ + $locator = $container['locator']; + + // Set locator to both streams. + Stream::setLocator($locator); + ReadOnlyStream::setLocator($locator); + + return new StreamBuilder($setup->getStreams()); + }; + } +} diff --git a/system/src/Grav/Common/Service/TaskServiceProvider.php b/system/src/Grav/Common/Service/TaskServiceProvider.php new file mode 100644 index 0000000..49ce147 --- /dev/null +++ b/system/src/Grav/Common/Service/TaskServiceProvider.php @@ -0,0 +1,55 @@ +getParsedBody(); + + $task = $body['task'] ?? $c['uri']->param('task'); + if (null !== $task) { + $task = filter_var($task, FILTER_SANITIZE_STRING); + } + + return $task ?: null; + }; + + $container['action'] = function (Grav $c) { + /** @var ServerRequestInterface $request */ + $request = $c['request']; + $body = $request->getParsedBody(); + + $action = $body['action'] ?? $c['uri']->param('action'); + if (null !== $action) { + $action = filter_var($action, FILTER_SANITIZE_STRING); + } + + return $action ?: null; + }; + } +} diff --git a/system/src/Grav/Common/Session.php b/system/src/Grav/Common/Session.php new file mode 100644 index 0000000..3221627 --- /dev/null +++ b/system/src/Grav/Common/Session.php @@ -0,0 +1,190 @@ +getInstance() method instead. + */ + public static function instance() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->getInstance() method instead', E_USER_DEPRECATED); + + return static::getInstance(); + } + + /** + * Initialize session. + * + * Code in this function has been moved into SessionServiceProvider class. + * + * @return void + */ + public function init() + { + if ($this->autoStart && !$this->isStarted()) { + $this->start(); + + $this->autoStart = false; + } + } + + /** + * @param bool $auto + * @return $this + */ + public function setAutoStart($auto) + { + $this->autoStart = (bool)$auto; + + return $this; + } + + /** + * Returns attributes. + * + * @return array Attributes + * @deprecated 1.5 Use ->getAll() method instead. + */ + public function all() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->getAll() method instead', E_USER_DEPRECATED); + + return $this->getAll(); + } + + /** + * Checks if the session was started. + * + * @return bool + * @deprecated 1.5 Use ->isStarted() method instead. + */ + public function started() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->isStarted() method instead', E_USER_DEPRECATED); + + return $this->isStarted(); + } + + /** + * Store something in session temporarily. + * + * @param string $name + * @param mixed $object + * @return $this + */ + public function setFlashObject($name, $object) + { + $this->__set($name, serialize($object)); + + return $this; + } + + /** + * Return object and remove it from session. + * + * @param string $name + * @return mixed + */ + public function getFlashObject($name) + { + $serialized = $this->__get($name); + + $object = is_string($serialized) ? unserialize($serialized, ['allowed_classes' => true]) : $serialized; + + $this->__unset($name); + + if ($name === 'files-upload') { + $grav = Grav::instance(); + + // Make sure that Forms 3.0+ has been installed. + if (null === $object && isset($grav['forms'])) { + user_error( + __CLASS__ . '::' . __FUNCTION__ . '(\'files-upload\') is deprecated since Grav 1.6, use $form->getFlash()->getLegacyFiles() instead', + E_USER_DEPRECATED + ); + + /** @var Uri $uri */ + $uri = $grav['uri']; + /** @var Forms|null $form */ + $form = $grav['forms']->getActiveForm(); // @phpstan-ignore-line (form plugin) + + $sessionField = base64_encode($uri->url); + + /** @var FormFlash|null $flash */ + $flash = $form ? $form->getFlash() : null; // @phpstan-ignore-line (form plugin) + $object = $flash && method_exists($flash, 'getLegacyFiles') ? [$sessionField => $flash->getLegacyFiles()] : null; + } + } + + return $object; + } + + /** + * Store something in cookie temporarily. + * + * @param string $name + * @param mixed $object + * @param int $time + * @return $this + * @throws JsonException + */ + public function setFlashCookieObject($name, $object, $time = 60) + { + setcookie($name, json_encode($object, JSON_THROW_ON_ERROR), $this->getCookieOptions($time)); + + return $this; + } + + /** + * Return object and remove it from the cookie. + * + * @param string $name + * @return mixed|null + * @throws JsonException + */ + public function getFlashCookieObject($name) + { + if (isset($_COOKIE[$name])) { + $cookie = $_COOKIE[$name]; + setcookie($name, '', $this->getCookieOptions(-42000)); + + return json_decode($cookie, false, 512, JSON_THROW_ON_ERROR); + } + + return null; + } + + /** + * @return void + */ + protected function onSessionStart(): void + { + $event = new SessionStartEvent($this); + + $grav = Grav::instance(); + $grav->dispatchEvent($event); + } +} diff --git a/system/src/Grav/Common/Taxonomy.php b/system/src/Grav/Common/Taxonomy.php new file mode 100644 index 0000000..0d0a450 --- /dev/null +++ b/system/src/Grav/Common/Taxonomy.php @@ -0,0 +1,176 @@ +taxonomy_map = []; + $this->grav = $grav; + } + + /** + * Takes an individual page and processes the taxonomies configured in its header. It + * then adds those taxonomies to the map + * + * @param PageInterface $page the page to process + * @param array|null $page_taxonomy + */ + public function addTaxonomy(PageInterface $page, $page_taxonomy = null) + { + if (!$page->published()) { + return; + } + + if (!$page_taxonomy) { + $page_taxonomy = $page->taxonomy(); + } + + if (empty($page_taxonomy)) { + return; + } + + /** @var Config $config */ + $config = $this->grav['config']; + $taxonomies = (array)$config->get('site.taxonomies'); + foreach ($taxonomies as $taxonomy) { + // Skip invalid taxonomies. + if (!\is_string($taxonomy)) { + continue; + } + $current = $page_taxonomy[$taxonomy] ?? null; + foreach ((array)$current as $item) { + $this->iterateTaxonomy($page, $taxonomy, '', $item); + } + } + } + + /** + * Iterate through taxonomy fields + * + * Reduces [taxonomy_type] to dot-notation where necessary + * + * @param PageInterface $page The Page to process + * @param string $taxonomy Taxonomy type to add + * @param string $key Taxonomy type to concatenate + * @param iterable|string $value Taxonomy value to add or iterate + * @return void + */ + public function iterateTaxonomy(PageInterface $page, string $taxonomy, string $key, $value) + { + if (is_iterable($value)) { + foreach ($value as $identifier => $item) { + $identifier = "{$key}.{$identifier}"; + $this->iterateTaxonomy($page, $taxonomy, $identifier, $item); + } + } elseif (is_string($value)) { + if (!empty($key)) { + $taxonomy .= $key; + } + $this->taxonomy_map[$taxonomy][(string) $value][$page->path()] = ['slug' => $page->slug()]; + } + } + + /** + * Returns a new Page object with the sub-pages containing all the values set for a + * particular taxonomy. + * + * @param array $taxonomies taxonomies to search, eg ['tag'=>['animal','cat']] + * @param string $operator can be 'or' or 'and' (defaults to 'and') + * @return Collection Collection object set to contain matches found in the taxonomy map + */ + public function findTaxonomy($taxonomies, $operator = 'and') + { + $matches = []; + $results = []; + + foreach ((array)$taxonomies as $taxonomy => $items) { + foreach ((array)$items as $item) { + if (isset($this->taxonomy_map[$taxonomy][$item])) { + $matches[] = $this->taxonomy_map[$taxonomy][$item]; + } else { + $matches[] = []; + } + } + } + + if (strtolower($operator) === 'or') { + foreach ($matches as $match) { + $results = array_merge($results, $match); + } + } else { + $results = $matches ? array_pop($matches) : []; + foreach ($matches as $match) { + $results = array_intersect_key($results, $match); + } + } + + return new Collection($results, ['taxonomies' => $taxonomies]); + } + + /** + * Gets and Sets the taxonomy map + * + * @param array|null $var the taxonomy map + * @return array the taxonomy map + */ + public function taxonomy($var = null) + { + if ($var) { + $this->taxonomy_map = $var; + } + + return $this->taxonomy_map; + } + + /** + * Gets item keys per taxonomy + * + * @param string $taxonomy taxonomy name + * @return array keys of this taxonomy + */ + public function getTaxonomyItemKeys($taxonomy) + { + return isset($this->taxonomy_map[$taxonomy]) ? array_keys($this->taxonomy_map[$taxonomy]) : []; + } +} diff --git a/system/src/Grav/Common/Theme.php b/system/src/Grav/Common/Theme.php new file mode 100644 index 0000000..a5006a2 --- /dev/null +++ b/system/src/Grav/Common/Theme.php @@ -0,0 +1,87 @@ +config["themes.{$this->name}"] ?? []; + } + + /** + * Persists to disk the theme parameters currently stored in the Grav Config object + * + * @param string $name The name of the theme whose config it should store. + * @return bool + */ + public static function saveConfig($name) + { + if (!$name) { + return false; + } + + $grav = Grav::instance(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + $filename = 'config://themes/' . $name . '.yaml'; + $file = YamlFile::instance((string)$locator->findResource($filename, true, true)); + $content = $grav['config']->get('themes.' . $name); + $file->save($content); + $file->free(); + unset($file); + + return true; + } + + /** + * Load blueprints. + * + * @return void + */ + protected function loadBlueprint() + { + if (!$this->blueprint) { + $grav = Grav::instance(); + /** @var Themes $themes */ + $themes = $grav['themes']; + $data = $themes->get($this->name); + \assert($data !== null); + $this->blueprint = $data->blueprints(); + } + } +} diff --git a/system/src/Grav/Common/Themes.php b/system/src/Grav/Common/Themes.php new file mode 100644 index 0000000..6126841 --- /dev/null +++ b/system/src/Grav/Common/Themes.php @@ -0,0 +1,417 @@ +grav = $grav; + $this->config = $grav['config']; + + // Register instance as autoloader for theme inheritance + spl_autoload_register([$this, 'autoloadTheme']); + } + + /** + * @return void + */ + public function init() + { + /** @var Themes $themes */ + $themes = $this->grav['themes']; + $themes->configure(); + + $this->initTheme(); + } + + /** + * @return void + */ + public function initTheme() + { + if ($this->inited === false) { + /** @var Themes $themes */ + $themes = $this->grav['themes']; + + try { + $instance = $themes->load(); + } catch (InvalidArgumentException $e) { + throw new RuntimeException($this->current() . ' theme could not be found'); + } + + // Register autoloader. + if (method_exists($instance, 'autoload')) { + $instance->autoload(); + } + + // Register event listeners. + if ($instance instanceof EventSubscriberInterface) { + /** @var EventDispatcher $events */ + $events = $this->grav['events']; + $events->addSubscriber($instance); + } + + // Register blueprints. + if (is_dir('theme://blueprints/pages')) { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + $locator->addPath('blueprints', '', ['theme://blueprints'], ['user', 'blueprints']); + } + + // Register form fields. + if (method_exists($instance, 'getFormFieldTypes')) { + /** @var Plugins $plugins */ + $plugins = $this->grav['plugins']; + $plugins->formFieldTypes = $instance->getFormFieldTypes() + $plugins->formFieldTypes; + } + + $this->grav['theme'] = $instance; + + $this->grav->fireEvent('onThemeInitialized'); + + $this->inited = true; + } + } + + /** + * Return list of all theme data with their blueprints. + * + * @return array + */ + public function all() + { + $list = []; + + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + $iterator = $locator->getIterator('themes://'); + + /** @var DirectoryIterator $directory */ + foreach ($iterator as $directory) { + if (!$directory->isDir() || $directory->isDot()) { + continue; + } + + $theme = $directory->getFilename(); + + try { + $result = $this->get($theme); + } catch (Exception $e) { + $exception = new RuntimeException(sprintf('Theme %s: %s', $theme, $e->getMessage()), $e->getCode(), $e); + + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addMessage("Theme {$theme} cannot be loaded, please check Exceptions tab", 'error'); + $debugger->addException($exception); + + continue; + } + + if ($result) { + $list[$theme] = $result; + } + } + ksort($list, SORT_NATURAL | SORT_FLAG_CASE); + + return $list; + } + + /** + * Get theme configuration or throw exception if it cannot be found. + * + * @param string $name + * @return Data|null + * @throws RuntimeException + */ + public function get($name) + { + if (!$name) { + throw new RuntimeException('Theme name not provided.'); + } + + $blueprints = new Blueprints('themes://'); + $blueprint = $blueprints->get("{$name}/blueprints"); + + // Load default configuration. + $file = CompiledYamlFile::instance("themes://{$name}/{$name}" . YAML_EXT); + + // ensure this is a valid theme + if (!$file->exists()) { + return null; + } + + // Find thumbnail. + $thumb = "themes://{$name}/thumbnail.jpg"; + $path = $this->grav['locator']->findResource($thumb, false); + + if ($path) { + $blueprint->set('thumbnail', $this->grav['base_url'] . '/' . $path); + } + + $obj = new Data((array)$file->content(), $blueprint); + + // Override with user configuration. + $obj->merge($this->config->get('themes.' . $name) ?: []); + + // Save configuration always to user/config. + $file = CompiledYamlFile::instance("config://themes/{$name}" . YAML_EXT); + $obj->file($file); + + return $obj; + } + + /** + * Return name of the current theme. + * + * @return string + */ + public function current() + { + return (string)$this->config->get('system.pages.theme'); + } + + /** + * Load current theme. + * + * @return Theme + */ + public function load() + { + // NOTE: ALL THE LOCAL VARIABLES ARE USED INSIDE INCLUDED FILE, DO NOT REMOVE THEM! + $grav = $this->grav; + $config = $this->config; + $name = $this->current(); + $class = null; + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + // Start by attempting to load the theme.php file. + $file = $locator('theme://theme.php') ?: $locator("theme://{$name}.php"); + if ($file) { + // Local variables available in the file: $grav, $config, $name, $file + $class = include $file; + if (!\is_object($class) || !is_subclass_of($class, Theme::class, true)) { + $class = null; + } + } elseif (!$locator('theme://') && !defined('GRAV_CLI')) { + $response = new Response(500, [], "Theme '$name' does not exist, unable to display page."); + + $grav->close($response); + } + + // If the class hasn't been initialized yet, guess the class name and create a new instance. + if (null === $class) { + $themeClassFormat = [ + 'Grav\\Theme\\' . Inflector::camelize($name), + 'Grav\\Theme\\' . ucfirst($name) + ]; + + foreach ($themeClassFormat as $themeClass) { + if (is_subclass_of($themeClass, Theme::class, true)) { + $class = new $themeClass($grav, $config, $name); + break; + } + } + } + + // Finally if everything else fails, just create a new instance from the default Theme class. + if (null === $class) { + $class = new Theme($grav, $config, $name); + } + + $this->config->set('theme', $config->get('themes.' . $name)); + + return $class; + } + + /** + * Configure and prepare streams for current template. + * + * @return void + * @throws InvalidArgumentException + */ + public function configure() + { + $name = $this->current(); + $config = $this->config; + + $this->loadConfiguration($name, $config); + + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + $registered = stream_get_wrappers(); + + $schemes = $config->get("themes.{$name}.streams.schemes", []); + $schemes += [ + 'theme' => [ + 'type' => 'ReadOnlyStream', + 'paths' => $locator->findResources("themes://{$name}", false) + ] + ]; + + foreach ($schemes as $scheme => $config) { + if (isset($config['paths'])) { + $locator->addPath($scheme, '', $config['paths']); + } + if (isset($config['prefixes'])) { + foreach ($config['prefixes'] as $prefix => $paths) { + $locator->addPath($scheme, $prefix, $paths); + } + } + + if (in_array($scheme, $registered, true)) { + stream_wrapper_unregister($scheme); + } + $type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream'; + if ($type[0] !== '\\') { + $type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type; + } + + if (!stream_wrapper_register($scheme, $type)) { + throw new InvalidArgumentException("Stream '{$type}' could not be initialized."); + } + } + + // Load languages after streams has been properly initialized + $this->loadLanguages($this->config); + } + + /** + * Load theme configuration. + * + * @param string $name Theme name + * @param Config $config Configuration class + * @return void + */ + protected function loadConfiguration($name, Config $config) + { + $themeConfig = CompiledYamlFile::instance("themes://{$name}/{$name}" . YAML_EXT)->content(); + $config->joinDefaults("themes.{$name}", $themeConfig); + } + + /** + * Load theme languages. + * Reads ALL language files from theme stream and merges them. + * + * @param Config $config Configuration class + * @return void + */ + protected function loadLanguages(Config $config) + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + if ($config->get('system.languages.translations', true)) { + $language_files = array_reverse($locator->findResources('theme://languages' . YAML_EXT)); + foreach ($language_files as $language_file) { + $language = CompiledYamlFile::instance($language_file)->content(); + $this->grav['languages']->mergeRecursive($language); + } + $languages_folders = array_reverse($locator->findResources('theme://languages')); + foreach ($languages_folders as $languages_folder) { + $languages = []; + $iterator = new DirectoryIterator($languages_folder); + foreach ($iterator as $file) { + if ($file->getExtension() !== 'yaml') { + continue; + } + $languages[$file->getBasename('.yaml')] = CompiledYamlFile::instance($file->getPathname())->content(); + } + $this->grav['languages']->mergeRecursive($languages); + } + } + } + + /** + * Autoload theme classes for inheritance + * + * @param string $class Class name + * @return mixed|false FALSE if unable to load $class; Class name if + * $class is successfully loaded + */ + protected function autoloadTheme($class) + { + $prefix = 'Grav\\Theme\\'; + if (false !== strpos($class, $prefix)) { + // Remove prefix from class + $class = substr($class, strlen($prefix)); + $locator = $this->grav['locator']; + + // First try lowercase version of the classname. + $path = strtolower($class); + $file = $locator("themes://{$path}/theme.php") ?: $locator("themes://{$path}/{$path}.php"); + + if ($file) { + return include_once $file; + } + + // Replace namespace tokens to directory separators + $path = $this->grav['inflector']->hyphenize($class); + $file = $locator("themes://{$path}/theme.php") ?: $locator("themes://{$path}/{$path}.php"); + + // Load class + if ($file) { + return include_once $file; + } + + // Try Old style theme classes + $path = preg_replace('#\\\|_(?!.+\\\)#', '/', $class); + \assert(null !== $path); + + $path = strtolower($path); + $file = $locator("themes://{$path}/theme.php") ?: $locator("themes://{$path}/{$path}.php"); + + // Load class + if ($file) { + return include_once $file; + } + } + + return false; + } +} diff --git a/system/src/Grav/Common/Twig/Exception/TwigException.php b/system/src/Grav/Common/Twig/Exception/TwigException.php new file mode 100644 index 0000000..8f543dc --- /dev/null +++ b/system/src/Grav/Common/Twig/Exception/TwigException.php @@ -0,0 +1,19 @@ +locator = Grav::instance()['locator']; + } + + /** + * @return TwigFilter[] + */ + public function getFilters() + { + return [ + new TwigFilter('file_exists', [$this, 'file_exists']), + new TwigFilter('fileatime', [$this, 'fileatime']), + new TwigFilter('filectime', [$this, 'filectime']), + new TwigFilter('filemtime', [$this, 'filemtime']), + new TwigFilter('filesize', [$this, 'filesize']), + new TwigFilter('filetype', [$this, 'filetype']), + new TwigFilter('is_dir', [$this, 'is_dir']), + new TwigFilter('is_file', [$this, 'is_file']), + new TwigFilter('is_link', [$this, 'is_link']), + new TwigFilter('is_readable', [$this, 'is_readable']), + new TwigFilter('is_writable', [$this, 'is_writable']), + new TwigFilter('is_writeable', [$this, 'is_writable']), + new TwigFilter('lstat', [$this, 'lstat']), + new TwigFilter('getimagesize', [$this, 'getimagesize']), + new TwigFilter('exif_read_data', [$this, 'exif_read_data']), + new TwigFilter('read_exif_data', [$this, 'exif_read_data']), + new TwigFilter('exif_imagetype', [$this, 'exif_imagetype']), + new TwigFilter('hash_file', [$this, 'hash_file']), + new TwigFilter('hash_hmac_file', [$this, 'hash_hmac_file']), + new TwigFilter('md5_file', [$this, 'md5_file']), + new TwigFilter('sha1_file', [$this, 'sha1_file']), + new TwigFilter('get_meta_tags', [$this, 'get_meta_tags']), + new TwigFilter('pathinfo', [$this, 'pathinfo']), + ]; + } + + /** + * Return a list of all functions. + * + * @return TwigFunction[] + */ + public function getFunctions() + { + return [ + new TwigFunction('file_exists', [$this, 'file_exists']), + new TwigFunction('fileatime', [$this, 'fileatime']), + new TwigFunction('filectime', [$this, 'filectime']), + new TwigFunction('filemtime', [$this, 'filemtime']), + new TwigFunction('filesize', [$this, 'filesize']), + new TwigFunction('filetype', [$this, 'filetype']), + new TwigFunction('is_dir', [$this, 'is_dir']), + new TwigFunction('is_file', [$this, 'is_file']), + new TwigFunction('is_link', [$this, 'is_link']), + new TwigFunction('is_readable', [$this, 'is_readable']), + new TwigFunction('is_writable', [$this, 'is_writable']), + new TwigFunction('is_writeable', [$this, 'is_writable']), + new TwigFunction('lstat', [$this, 'lstat']), + new TwigFunction('getimagesize', [$this, 'getimagesize']), + new TwigFunction('exif_read_data', [$this, 'exif_read_data']), + new TwigFunction('read_exif_data', [$this, 'exif_read_data']), + new TwigFunction('exif_imagetype', [$this, 'exif_imagetype']), + new TwigFunction('hash_file', [$this, 'hash_file']), + new TwigFunction('hash_hmac_file', [$this, 'hash_hmac_file']), + new TwigFunction('md5_file', [$this, 'md5_file']), + new TwigFunction('sha1_file', [$this, 'sha1_file']), + new TwigFunction('get_meta_tags', [$this, 'get_meta_tags']), + new TwigFunction('pathinfo', [$this, 'pathinfo']), + ]; + } + + /** + * @param string $filename + * @return bool + */ + public function file_exists($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return file_exists($filename); + } + + /** + * @param string $filename + * @return int|false + */ + public function fileatime($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return fileatime($filename); + } + + /** + * @param string $filename + * @return int|false + */ + public function filectime($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return filectime($filename); + } + + /** + * @param string $filename + * @return int|false + */ + public function filemtime($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return filemtime($filename); + } + + /** + * @param string $filename + * @return int|false + */ + public function filesize($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return filesize($filename); + } + + /** + * @param string $filename + * @return string|false + */ + public function filetype($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return filetype($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_dir($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_dir($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_file($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_file($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_link($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_link($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_readable($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_readable($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_writable($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_writable($filename); + } + + /** + * @param string $filename + * @return array|false + */ + public function lstat($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return lstat($filename); + } + + /** + * @param string $filename + * @return array|false + */ + public function getimagesize($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return getimagesize($filename); + } + + /** + * @param string $file + * @param string|null $required_sections + * @param bool $as_arrays + * @param bool $read_thumbnail + * @return array|false + */ + public function exif_read_data($file, ?string $required_sections, bool $as_arrays = false, bool $read_thumbnail = false) + { + if (!Utils::functionExists('exif_read_data') || !$this->checkFilename($file)) { + return false; + } + + return exif_read_data($file, $required_sections, $as_arrays, $read_thumbnail); + } + + /** + * @param string $filename + * @return string|false + */ + public function exif_imagetype($filename) + { + if (!Utils::functionExists('exif_imagetype') || !$this->checkFilename($filename)) { + return false; + } + + return @exif_imagetype($filename); + } + + /** + * @param string $algo + * @param string $filename + * @param bool $binary + * @return string|false + */ + public function hash_file(string $algo, string $filename, bool $binary = false) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return hash_file($algo, $filename, $binary); + } + + /** + * @param string $algo + * @param string $data + * @param string $key + * @param bool $binary + * @return string|false + */ + public function hash_hmac_file(string $algo, string $data, string $key, bool $binary = false) + { + if (!$this->checkFilename($data)) { + return false; + } + + return hash_hmac_file($algo, $data, $key, $binary); + } + + /** + * @param string $filename + * @param bool $binary + * @return string|false + */ + public function md5_file($filename, bool $binary = false) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return md5_file($filename, $binary); + } + + /** + * @param string $filename + * @param bool $binary + * @return string|false + */ + public function sha1_file($filename, bool $binary = false) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return sha1_file($filename, $binary); + } + + /** + * @param string $filename + * @return array|false + */ + public function get_meta_tags($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return get_meta_tags($filename); + } + + /** + * @param string $path + * @param int|null $flags + * @return string|string[] + */ + public function pathinfo($path, $flags = null) + { + if (null !== $flags) { + return pathinfo($path, (int)$flags); + } + + return pathinfo($path); + } + + /** + * @param string $filename + * @return bool + */ + private function checkFilename($filename): bool + { + return is_string($filename) && (!str_contains($filename, '://') || $this->locator->isStream($filename)); + } +} diff --git a/system/src/Grav/Common/Twig/Extension/GravExtension.php b/system/src/Grav/Common/Twig/Extension/GravExtension.php new file mode 100644 index 0000000..4c0d9fe --- /dev/null +++ b/system/src/Grav/Common/Twig/Extension/GravExtension.php @@ -0,0 +1,1641 @@ +grav = Grav::instance(); + $this->debugger = $this->grav['debugger'] ?? null; + $this->config = $this->grav['config']; + } + + /** + * Register some standard globals + * + * @return array + */ + public function getGlobals(): array + { + return [ + 'grav' => $this->grav, + ]; + } + + /** + * Return a list of all filters. + * + * @return array + */ + public function getFilters(): array + { + return [ + new TwigFilter('*ize', [$this, 'inflectorFilter']), + new TwigFilter('absolute_url', [$this, 'absoluteUrlFilter']), + new TwigFilter('contains', [$this, 'containsFilter']), + new TwigFilter('chunk_split', [$this, 'chunkSplitFilter']), + new TwigFilter('nicenumber', [$this, 'niceNumberFunc']), + new TwigFilter('nicefilesize', [$this, 'niceFilesizeFunc']), + new TwigFilter('nicetime', [$this, 'nicetimeFunc']), + new TwigFilter('defined', [$this, 'definedDefaultFilter']), + new TwigFilter('ends_with', [$this, 'endsWithFilter']), + new TwigFilter('fieldName', [$this, 'fieldNameFilter']), + new TwigFilter('ksort', [$this, 'ksortFilter']), + new TwigFilter('ltrim', [$this, 'ltrimFilter']), + new TwigFilter('markdown', [$this, 'markdownFunction'], ['needs_context' => true, 'is_safe' => ['html']]), + new TwigFilter('md5', [$this, 'md5Filter']), + new TwigFilter('base32_encode', [$this, 'base32EncodeFilter']), + new TwigFilter('base32_decode', [$this, 'base32DecodeFilter']), + new TwigFilter('base64_encode', [$this, 'base64EncodeFilter']), + new TwigFilter('base64_decode', [$this, 'base64DecodeFilter']), + new TwigFilter('randomize', [$this, 'randomizeFilter']), + new TwigFilter('modulus', [$this, 'modulusFilter']), + new TwigFilter('rtrim', [$this, 'rtrimFilter']), + new TwigFilter('pad', [$this, 'padFilter']), + new TwigFilter('regex_replace', [$this, 'regexReplace']), + new TwigFilter('safe_email', [$this, 'safeEmailFilter'], ['is_safe' => ['html']]), + new TwigFilter('safe_truncate', [Utils::class, 'safeTruncate']), + new TwigFilter('safe_truncate_html', [Utils::class, 'safeTruncateHTML']), + new TwigFilter('sort_by_key', [$this, 'sortByKeyFilter']), + new TwigFilter('starts_with', [$this, 'startsWithFilter']), + new TwigFilter('truncate', [Utils::class, 'truncate']), + new TwigFilter('truncate_html', [Utils::class, 'truncateHTML']), + new TwigFilter('json_decode', [$this, 'jsonDecodeFilter']), + new TwigFilter('array_unique', 'array_unique'), + new TwigFilter('basename', 'basename'), + new TwigFilter('dirname', 'dirname'), + new TwigFilter('print_r', [$this, 'print_r']), + new TwigFilter('yaml_encode', [$this, 'yamlEncodeFilter']), + new TwigFilter('yaml_decode', [$this, 'yamlDecodeFilter']), + new TwigFilter('nicecron', [$this, 'niceCronFilter']), + + // Translations + new TwigFilter('t', [$this, 'translate'], ['needs_environment' => true]), + new TwigFilter('tl', [$this, 'translateLanguage']), + new TwigFilter('ta', [$this, 'translateArray']), + + // Casting values + new TwigFilter('string', [$this, 'stringFilter']), + new TwigFilter('int', [$this, 'intFilter'], ['is_safe' => ['all']]), + new TwigFilter('bool', [$this, 'boolFilter']), + new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]), + new TwigFilter('array', [$this, 'arrayFilter']), + new TwigFilter('yaml', [$this, 'yamlFilter']), + + // Object Types + new TwigFilter('get_type', [$this, 'getTypeFunc']), + new TwigFilter('of_type', [$this, 'ofTypeFunc']), + + // PHP methods + new TwigFilter('count', 'count'), + new TwigFilter('array_diff', 'array_diff'), + ]; + } + + /** + * Return a list of all functions. + * + * @return array + */ + public function getFunctions(): array + { + return [ + new TwigFunction('array', [$this, 'arrayFilter']), + new TwigFunction('array_key_value', [$this, 'arrayKeyValueFunc']), + new TwigFunction('array_key_exists', 'array_key_exists'), + new TwigFunction('array_unique', 'array_unique'), + new TwigFunction('array_intersect', [$this, 'arrayIntersectFunc']), + new TwigFunction('array_diff', 'array_diff'), + new TwigFunction('authorize', [$this, 'authorize']), + new TwigFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]), + new TwigFunction('dump', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]), + new TwigFunction('vardump', [$this, 'vardumpFunc']), + new TwigFunction('print_r', [$this, 'print_r']), + new TwigFunction('http_response_code', 'http_response_code'), + new TwigFunction('evaluate', [$this, 'evaluateStringFunc'], ['needs_context' => true]), + new TwigFunction('evaluate_twig', [$this, 'evaluateTwigFunc'], ['needs_context' => true]), + new TwigFunction('gist', [$this, 'gistFunc']), + new TwigFunction('nonce_field', [$this, 'nonceFieldFunc']), + new TwigFunction('pathinfo', 'pathinfo'), + new TwigFunction('random_string', [$this, 'randomStringFunc']), + new TwigFunction('repeat', [$this, 'repeatFunc']), + new TwigFunction('regex_replace', [$this, 'regexReplace']), + new TwigFunction('regex_filter', [$this, 'regexFilter']), + new TwigFunction('regex_match', [$this, 'regexMatch']), + new TwigFunction('regex_split', [$this, 'regexSplit']), + new TwigFunction('string', [$this, 'stringFilter']), + new TwigFunction('url', [$this, 'urlFunc']), + new TwigFunction('json_decode', [$this, 'jsonDecodeFilter']), + new TwigFunction('get_cookie', [$this, 'getCookie']), + new TwigFunction('redirect_me', [$this, 'redirectFunc']), + new TwigFunction('range', [$this, 'rangeFunc']), + new TwigFunction('isajaxrequest', [$this, 'isAjaxFunc']), + new TwigFunction('exif', [$this, 'exifFunc']), + new TwigFunction('media_directory', [$this, 'mediaDirFunc']), + new TwigFunction('body_class', [$this, 'bodyClassFunc'], ['needs_context' => true]), + new TwigFunction('theme_var', [$this, 'themeVarFunc'], ['needs_context' => true]), + new TwigFunction('header_var', [$this, 'pageHeaderVarFunc'], ['needs_context' => true]), + new TwigFunction('read_file', [$this, 'readFileFunc']), + new TwigFunction('nicenumber', [$this, 'niceNumberFunc']), + new TwigFunction('nicefilesize', [$this, 'niceFilesizeFunc']), + new TwigFunction('nicetime', [$this, 'nicetimeFunc']), + new TwigFunction('cron', [$this, 'cronFunc']), + new TwigFunction('svg_image', [$this, 'svgImageFunction']), + new TwigFunction('xss', [$this, 'xssFunc']), + new TwigFunction('unique_id', [$this, 'uniqueId']), + + // Translations + new TwigFunction('t', [$this, 'translate'], ['needs_environment' => true]), + new TwigFunction('tl', [$this, 'translateLanguage']), + new TwigFunction('ta', [$this, 'translateArray']), + + // Object Types + new TwigFunction('get_type', [$this, 'getTypeFunc']), + new TwigFunction('of_type', [$this, 'ofTypeFunc']), + + // PHP methods + new TwigFunction('is_numeric', 'is_numeric'), + new TwigFunction('is_iterable', 'is_iterable'), + new TwigFunction('is_countable', 'is_countable'), + new TwigFunction('is_null', 'is_null'), + new TwigFunction('is_string', 'is_string'), + new TwigFunction('is_array', 'is_array'), + new TwigFunction('is_object', 'is_object'), + new TwigFunction('count', 'count'), + new TwigFunction('array_diff', 'array_diff'), + ]; + } + + /** + * @return array + */ + public function getTokenParsers(): array + { + return [ + new TwigTokenParserRender(), + new TwigTokenParserThrow(), + new TwigTokenParserTryCatch(), + new TwigTokenParserScript(), + new TwigTokenParserStyle(), + new TwigTokenParserMarkdown(), + new TwigTokenParserSwitch(), + new TwigTokenParserCache(), + ]; + } + + public function print_r($var) + { + return print_r($var, true); + } + + /** + * Filters field name by changing dot notation into array notation. + * + * @param string $str + * @return string + */ + public function fieldNameFilter($str) + { + $path = explode('.', rtrim($str, '.')); + + return array_shift($path) . ($path ? '[' . implode('][', $path) . ']' : ''); + } + + /** + * Protects email address. + * + * @param string $str + * @return string + */ + public function safeEmailFilter($str) + { + static $list = [ + '"' => '"', + "'" => ''', + '&' => '&', + '<' => '<', + '>' => '>', + '@' => '@' + ]; + + $characters = mb_str_split($str, 1, 'UTF-8'); + + $encoded = ''; + foreach ($characters as $chr) { + $encoded .= $list[$chr] ?? (random_int(0, 1) ? '&#' . mb_ord($chr) . ';' : $chr); + } + + return $encoded; + } + + /** + * Returns array in a random order. + * + * @param array|Traversable $original + * @param int $offset Can be used to return only slice of the array. + * @return array + */ + public function randomizeFilter($original, $offset = 0) + { + if ($original instanceof Traversable) { + $original = iterator_to_array($original, false); + } + + if (!is_array($original)) { + return $original; + } + + $sorted = []; + $random = array_slice($original, $offset); + shuffle($random); + + $sizeOf = count($original); + for ($x = 0; $x < $sizeOf; $x++) { + if ($x < $offset) { + $sorted[] = $original[$x]; + } else { + $sorted[] = array_shift($random); + } + } + + return $sorted; + } + + /** + * Returns the modulus of an integer + * + * @param string|int $number + * @param int $divider + * @param array|null $items array of items to select from to return + * @return int + */ + public function modulusFilter($number, $divider, $items = null) + { + if (is_string($number)) { + $number = strlen($number); + } + + $remainder = $number % $divider; + + if (is_array($items)) { + return $items[$remainder] ?? $items[0]; + } + + return $remainder; + } + + /** + * Inflector supports following notations: + * + * `{{ 'person'|pluralize }} => people` + * `{{ 'shoes'|singularize }} => shoe` + * `{{ 'welcome page'|titleize }} => "Welcome Page"` + * `{{ 'send_email'|camelize }} => SendEmail` + * `{{ 'CamelCased'|underscorize }} => camel_cased` + * `{{ 'Something Text'|hyphenize }} => something-text` + * `{{ 'something_text_to_read'|humanize }} => "Something text to read"` + * `{{ '181'|monthize }} => 5` + * `{{ '10'|ordinalize }} => 10th` + * + * @param string $action + * @param string $data + * @param int|null $count + * @return string + */ + public function inflectorFilter($action, $data, $count = null) + { + $action .= 'ize'; + + /** @var Inflector $inflector */ + $inflector = $this->grav['inflector']; + + if (in_array( + $action, + ['titleize', 'camelize', 'underscorize', 'hyphenize', 'humanize', 'ordinalize', 'monthize'], + true + )) { + return $inflector->{$action}($data); + } + + if (in_array($action, ['pluralize', 'singularize'], true)) { + return $count ? $inflector->{$action}($data, $count) : $inflector->{$action}($data); + } + + return $data; + } + + /** + * Return MD5 hash from the input. + * + * @param string $str + * @return string + */ + public function md5Filter($str) + { + return md5($str); + } + + /** + * Return Base32 encoded string + * + * @param string $str + * @return string + */ + public function base32EncodeFilter($str) + { + return Base32::encode($str); + } + + /** + * Return Base32 decoded string + * + * @param string $str + * @return string + */ + public function base32DecodeFilter($str) + { + return Base32::decode($str); + } + + /** + * Return Base64 encoded string + * + * @param string $str + * @return string + */ + public function base64EncodeFilter($str) + { + return base64_encode($str); + } + + /** + * Return Base64 decoded string + * + * @param string $str + * @return string|false + */ + public function base64DecodeFilter($str) + { + return base64_decode($str); + } + + /** + * Sorts a collection by key + * + * @param array $input + * @param string $filter + * @param int $direction + * @param int $sort_flags + * @return array + */ + public function sortByKeyFilter($input, $filter, $direction = SORT_ASC, $sort_flags = SORT_REGULAR) + { + return Utils::sortArrayByKey($input, $filter, $direction, $sort_flags); + } + + /** + * Return ksorted collection. + * + * @param array|null $array + * @return array + */ + public function ksortFilter($array) + { + if (null === $array) { + $array = []; + } + ksort($array); + + return $array; + } + + /** + * Wrapper for chunk_split() function + * + * @param string $value + * @param int $chars + * @param string $split + * @return string + */ + public function chunkSplitFilter($value, $chars, $split = '-') + { + return chunk_split($value, $chars, $split); + } + + /** + * determine if a string contains another + * + * @param string $haystack + * @param string $needle + * @return string|bool + * @todo returning $haystack here doesn't make much sense + */ + public function containsFilter($haystack, $needle) + { + if (empty($needle)) { + return $haystack; + } + + return (strpos($haystack, (string) $needle) !== false); + } + + /** + * Gets a human readable output for cron syntax + * + * @param string $at + * @return string + */ + public function niceCronFilter($at) + { + $cron = new Cron($at); + return $cron->getText('en'); + } + + /** + * Get Cron object for a crontab 'at' format + * + * @param string $at + * @return CronExpression + */ + public function cronFunc($at) + { + return CronExpression::factory($at); + } + + /** + * displays a facebook style 'time ago' formatted date/time + * + * @param string $date + * @param bool $long_strings + * @param bool $show_tense + * @return string + */ + public function nicetimeFunc($date, $long_strings = true, $show_tense = true) + { + if (empty($date)) { + return $this->grav['language']->translate('GRAV.NICETIME.NO_DATE_PROVIDED'); + } + + if ($long_strings) { + $periods = [ + 'NICETIME.SECOND', + 'NICETIME.MINUTE', + 'NICETIME.HOUR', + 'NICETIME.DAY', + 'NICETIME.WEEK', + 'NICETIME.MONTH', + 'NICETIME.YEAR', + 'NICETIME.DECADE' + ]; + } else { + $periods = [ + 'NICETIME.SEC', + 'NICETIME.MIN', + 'NICETIME.HR', + 'NICETIME.DAY', + 'NICETIME.WK', + 'NICETIME.MO', + 'NICETIME.YR', + 'NICETIME.DEC' + ]; + } + + $lengths = ['60', '60', '24', '7', '4.35', '12', '10']; + + $now = time(); + + // check if unix timestamp + if ((string)(int)$date === (string)$date) { + $unix_date = $date; + } else { + $unix_date = strtotime($date); + } + + // check validity of date + if (empty($unix_date)) { + return $this->grav['language']->translate('GRAV.NICETIME.BAD_DATE'); + } + + // is it future date or past date + if ($now > $unix_date) { + $difference = $now - $unix_date; + $tense = $this->grav['language']->translate('GRAV.NICETIME.AGO'); + } elseif ($now == $unix_date) { + $difference = $now - $unix_date; + $tense = $this->grav['language']->translate('GRAV.NICETIME.JUST_NOW'); + } else { + $difference = $unix_date - $now; + $tense = $this->grav['language']->translate('GRAV.NICETIME.FROM_NOW'); + } + + for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths) - 1; $j++) { + $difference /= $lengths[$j]; + } + + $difference = round($difference); + + if ($difference != 1) { + $periods[$j] .= '_PLURAL'; + } + + if ($this->grav['language']->getTranslation( + $this->grav['language']->getLanguage(), + $periods[$j] . '_MORE_THAN_TWO' + ) + ) { + if ($difference > 2) { + $periods[$j] .= '_MORE_THAN_TWO'; + } + } + + $periods[$j] = $this->grav['language']->translate('GRAV.'.$periods[$j]); + + if ($now == $unix_date) { + return $tense; + } + + $time = "{$difference} {$periods[$j]}"; + $time .= $show_tense ? " {$tense}" : ''; + + return $time; + } + + /** + * Allow quick check of a string for XSS Vulnerabilities + * + * @param string|array $data + * @return bool|string|array + */ + public function xssFunc($data) + { + if (!is_array($data)) { + return Security::detectXss($data); + } + + $results = Security::detectXssFromArray($data); + $results_parts = array_map(static function ($value, $key) { + return $key.': \''.$value . '\''; + }, array_values($results), array_keys($results)); + + return implode(', ', $results_parts); + } + + /** + * Generates a random string with configurable length, prefix and suffix. + * Unlike the built-in `uniqid()`, this string is non-conflicting and safe + * + * @param int $length + * @param array $options + * @return string + * @throws \Exception + */ + public function uniqueId(int $length = 9, array $options = ['prefix' => '', 'suffix' => '']): string + { + return Utils::uniqueId($length, $options); + } + + /** + * @param string $string + * @return string + */ + public function absoluteUrlFilter($string) + { + $url = $this->grav['uri']->base(); + $string = preg_replace('/((?:href|src) *= *[\'"](?!(http|ftp)))/i', "$1$url", $string); + + return $string; + } + + /** + * @param array $context + * @param string $string + * @param bool $block Block or Line processing + * @return string + */ + public function markdownFunction($context, $string, $block = true) + { + $page = $context['page'] ?? null; + return Utils::processMarkdown($string, $block, $page); + } + + /** + * @param string $haystack + * @param string $needle + * @return bool + */ + public function startsWithFilter($haystack, $needle) + { + return Utils::startsWith($haystack, $needle); + } + + /** + * @param string $haystack + * @param string $needle + * @return bool + */ + public function endsWithFilter($haystack, $needle) + { + return Utils::endsWith($haystack, $needle); + } + + /** + * @param mixed $value + * @param null $default + * @return mixed|null + */ + public function definedDefaultFilter($value, $default = null) + { + return $value ?? $default; + } + + /** + * @param string $value + * @param string|null $chars + * @return string + */ + public function rtrimFilter($value, $chars = null) + { + return null !== $chars ? rtrim($value, $chars) : rtrim($value); + } + + /** + * @param string $value + * @param string|null $chars + * @return string + */ + public function ltrimFilter($value, $chars = null) + { + return null !== $chars ? ltrim($value, $chars) : ltrim($value); + } + + /** + * Returns a string from a value. If the value is array, return it json encoded + * + * @param mixed $value + * @return string + */ + public function stringFilter($value) + { + // Format the array as a string + if (is_array($value)) { + return json_encode($value); + } + + // Boolean becomes '1' or '0' + if (is_bool($value)) { + $value = (int)$value; + } + + // Cast the other values to string. + return (string)$value; + } + + /** + * Casts input to int. + * + * @param mixed $input + * @return int + */ + public function intFilter($input) + { + return (int) $input; + } + + /** + * Casts input to bool. + * + * @param mixed $input + * @return bool + */ + public function boolFilter($input) + { + return (bool) $input; + } + + /** + * Casts input to float. + * + * @param mixed $input + * @return float + */ + public function floatFilter($input) + { + return (float) $input; + } + + /** + * Casts input to array. + * + * @param mixed $input + * @return array + */ + public function arrayFilter($input) + { + if (is_array($input)) { + return $input; + } + + if (is_object($input)) { + if (method_exists($input, 'toArray')) { + return $input->toArray(); + } + + if ($input instanceof Iterator) { + return iterator_to_array($input); + } + } + + return (array)$input; + } + + /** + * @param array|object $value + * @param int|null $inline + * @param int|null $indent + * @return string + */ + public function yamlFilter($value, $inline = null, $indent = null): string + { + return Yaml::dump($value, $inline, $indent); + } + + /** + * @param Environment $twig + * @return string + */ + public function translate(Environment $twig, ...$args) + { + // If admin and tu filter provided, use it + if (isset($this->grav['admin'])) { + $numargs = count($args); + $lang = null; + + if (($numargs === 3 && is_array($args[1])) || ($numargs === 2 && !is_array($args[1]))) { + $lang = array_pop($args); + /** @var Language $language */ + $language = $this->grav['language']; + if (is_string($lang) && !$language->getLanguageCode($lang)) { + $args[] = $lang; + $lang = null; + } + } elseif ($numargs === 2 && is_array($args[1])) { + $subs = array_pop($args); + $args = array_merge($args, $subs); + } + + return $this->grav['admin']->translate($args, $lang); + } + + // else use the default grav translate functionality + return $this->grav['language']->translate($args); + } + + /** + * Translate Strings + * + * @param string|array $args + * @param array|null $languages + * @param bool $array_support + * @param bool $html_out + * @return string + */ + public function translateLanguage($args, array $languages = null, $array_support = false, $html_out = false) + { + /** @var Language $language */ + $language = $this->grav['language']; + + return $language->translate($args, $languages, $array_support, $html_out); + } + + /** + * @param string $key + * @param string $index + * @param array|null $lang + * @return string + */ + public function translateArray($key, $index, $lang = null) + { + /** @var Language $language */ + $language = $this->grav['language']; + + return $language->translateArray($key, $index, $lang); + } + + /** + * Repeat given string x times. + * + * @param string $input + * @param int $multiplier + * + * @return string + */ + public function repeatFunc($input, $multiplier) + { + return str_repeat($input, $multiplier); + } + + /** + * Return URL to the resource. + * + * @example {{ url('theme://images/logo.png')|default('http://www.placehold.it/150x100/f4f4f4') }} + * + * @param string $input Resource to be located. + * @param bool $domain True to include domain name. + * @param bool $failGracefully If true, return URL even if the file does not exist. + * @return string|false Returns url to the resource or null if resource was not found. + */ + public function urlFunc($input, $domain = false, $failGracefully = false) + { + return Utils::url($input, $domain, $failGracefully); + } + + /** + * This function will evaluate Twig $twig through the $environment, and return its results. + * + * @param array $context + * @param string $twig + * @return mixed + */ + public function evaluateTwigFunc($context, $twig) + { + + $loader = new FilesystemLoader('.'); + $env = new Environment($loader); + $env->addExtension($this); + + $template = $env->createTemplate($twig); + + return $template->render($context); + } + + /** + * This function will evaluate a $string through the $environment, and return its results. + * + * @param array $context + * @param string $string + * @return mixed + */ + public function evaluateStringFunc($context, $string) + { + return $this->evaluateTwigFunc($context, "{{ $string }}"); + } + + /** + * Based on Twig\Extension\Debug / twig_var_dump + * (c) 2011 Fabien Potencier + * + * @param Environment $env + * @param array $context + */ + public function dump(Environment $env, $context) + { + if (!$env->isDebug() || !$this->debugger) { + return; + } + + $count = func_num_args(); + if (2 === $count) { + $data = []; + foreach ($context as $key => $value) { + if (is_object($value)) { + if (method_exists($value, 'toArray')) { + $data[$key] = $value->toArray(); + } else { + $data[$key] = 'Object (' . get_class($value) . ')'; + } + } else { + $data[$key] = $value; + } + } + $this->debugger->addMessage($data, 'debug'); + } else { + for ($i = 2; $i < $count; $i++) { + $var = func_get_arg($i); + $this->debugger->addMessage($var, 'debug'); + } + } + } + + /** + * Output a Gist + * + * @param string $id + * @param string|false $file + * @return string + */ + public function gistFunc($id, $file = false) + { + $url = 'https://gist.github.com/' . $id . '.js'; + if ($file) { + $url .= '?file=' . $file; + } + return ''; + } + + /** + * Generate a random string + * + * @param int $count + * @return string + */ + public function randomStringFunc($count = 5) + { + return Utils::generateRandomString($count); + } + + /** + * Pad a string to a certain length with another string + * + * @param string $input + * @param int $pad_length + * @param string $pad_string + * @param int $pad_type + * @return string + */ + public static function padFilter($input, $pad_length, $pad_string = ' ', $pad_type = STR_PAD_RIGHT) + { + return str_pad($input, (int)$pad_length, $pad_string, $pad_type); + } + + /** + * Workaround for twig associative array initialization + * Returns a key => val array + * + * @param string $key key of item + * @param string $val value of item + * @param array|null $current_array optional array to add to + * @return array + */ + public function arrayKeyValueFunc($key, $val, $current_array = null) + { + if (empty($current_array)) { + return array($key => $val); + } + + $current_array[$key] = $val; + + return $current_array; + } + + /** + * Wrapper for array_intersect() method + * + * @param array|Collection $array1 + * @param array|Collection $array2 + * @return array|Collection + */ + public function arrayIntersectFunc($array1, $array2) + { + if ($array1 instanceof Collection && $array2 instanceof Collection) { + return $array1->intersect($array2)->toArray(); + } + + return array_intersect($array1, $array2); + } + + /** + * Translate a string + * + * @return string + */ + public function translateFunc() + { + return $this->grav['language']->translate(func_get_args()); + } + + /** + * Authorize an action. Returns true if the user is logged in and + * has the right to execute $action. + * + * @param string|array $action An action or a list of actions. Each + * entry can be a string like 'group.action' + * or without dot notation an associative + * array. + * @return bool Returns TRUE if the user is authorized to + * perform the action, FALSE otherwise. + */ + public function authorize($action) + { + // Admin can use Flex users even if the site does not; make sure we use the right version of the user. + $admin = $this->grav['admin'] ?? null; + if ($admin) { + $user = $admin->user; + } else { + /** @var UserInterface|null $user */ + $user = $this->grav['user'] ?? null; + } + + if (!$user) { + return false; + } + + if (is_array($action)) { + if (Utils::isAssoc($action)) { + // Handle nested access structure. + $actions = Utils::arrayFlattenDotNotation($action); + } else { + // Handle simple access list. + $actions = array_combine($action, array_fill(0, count($action), true)); + } + } else { + // Handle single action. + $actions = [(string)$action => true]; + } + + $count = count($actions); + foreach ($actions as $act => $authenticated) { + // Ignore 'admin.super' if it's not the only value to be checked. + if ($act === 'admin.super' && $count > 1 && $user instanceof FlexObjectInterface) { + continue; + } + + $auth = $user->authorize($act) ?? false; + if (is_bool($auth) && $auth === Utils::isPositive($authenticated)) { + return true; + } + } + + return false; + } + + /** + * Used to add a nonce to a form. Call {{ nonce_field('action') }} specifying a string representing the action. + * + * For maximum protection, ensure that the string representing the action is as specific as possible + * + * @param string $action the action + * @param string $nonceParamName a custom nonce param name + * @return string the nonce input field + */ + public function nonceFieldFunc($action, $nonceParamName = 'nonce') + { + $string = ''; + + return $string; + } + + /** + * Decodes string from JSON. + * + * @param string $str + * @param bool $assoc + * @param int $depth + * @param int $options + * @return array + */ + public function jsonDecodeFilter($str, $assoc = false, $depth = 512, $options = 0) + { + return json_decode(html_entity_decode($str, ENT_COMPAT | ENT_HTML401, 'UTF-8'), $assoc, $depth, $options); + } + + /** + * Used to retrieve a cookie value + * + * @param string $key The cookie name to retrieve + * @return string + */ + public function getCookie($key) + { + return filter_input(INPUT_COOKIE, $key, FILTER_SANITIZE_STRING); + } + + /** + * Twig wrapper for PHP's preg_replace method + * + * @param string|string[] $subject the content to perform the replacement on + * @param string|string[] $pattern the regex pattern to use for matches + * @param string|string[] $replace the replacement value either as a string or an array of replacements + * @param int $limit the maximum possible replacements for each pattern in each subject + * @return string|string[]|null the resulting content + */ + public function regexReplace($subject, $pattern, $replace, $limit = -1) + { + return preg_replace($pattern, $replace, $subject, $limit); + } + + /** + * Twig wrapper for PHP's preg_grep method + * + * @param array $array + * @param string $regex + * @param int $flags + * @return array + */ + public function regexFilter($array, $regex, $flags = 0) + { + return preg_grep($regex, $array, $flags); + } + + /** + * Twig wrapper for PHP's preg_match method + * + * @param string $subject the content to perform the match on + * @param string $pattern the regex pattern to use for match + * @param int $flags + * @param int $offset + * @return array|false returns the matches if there is at least one match in the subject for a given pattern or null if not. + */ + public function regexMatch($subject, $pattern, $flags = 0, $offset = 0) + { + if (preg_match($pattern, $subject, $matches, $flags, $offset) === false) { + return false; + } + + return $matches; + } + + /** + * Twig wrapper for PHP's preg_split method + * + * @param string $subject the content to perform the split on + * @param string $pattern the regex pattern to use for split + * @param int $limit the maximum possible splits for the given pattern + * @param int $flags + * @return array|false the resulting array after performing the split operation + */ + public function regexSplit($subject, $pattern, $limit = -1, $flags = 0) + { + return preg_split($pattern, $subject, $limit, $flags); + } + + /** + * redirect browser from twig + * + * @param string $url the url to redirect to + * @param int $statusCode statusCode, default 303 + * @return void + */ + public function redirectFunc($url, $statusCode = 303) + { + $response = new Response($statusCode, ['location' => $url]); + + $this->grav->close($response); + } + + /** + * Generates an array containing a range of elements, optionally stepped + * + * @param int $start Minimum number, default 0 + * @param int $end Maximum number, default `getrandmax()` + * @param int $step Increment between elements in the sequence, default 1 + * @return array + */ + public function rangeFunc($start = 0, $end = 100, $step = 1) + { + return range($start, $end, $step); + } + + /** + * Check if HTTP_X_REQUESTED_WITH has been set to xmlhttprequest, + * in which case we may unsafely assume ajax. Non critical use only. + * + * @return bool True if HTTP_X_REQUESTED_WITH exists and has been set to xmlhttprequest + */ + public function isAjaxFunc() + { + return ( + !empty($_SERVER['HTTP_X_REQUESTED_WITH']) + && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); + } + + /** + * Get the Exif data for a file + * + * @param string $image + * @param bool $raw + * @return mixed + */ + public function exifFunc($image, $raw = false) + { + if (isset($this->grav['exif'])) { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + if ($locator->isStream($image)) { + $image = $locator->findResource($image); + } + + $exif_reader = $this->grav['exif']->getReader(); + + if ($image && file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) { + $exif_data = $exif_reader->read($image); + + if ($exif_data) { + if ($raw) { + return $exif_data->getRawData(); + } + + return $exif_data->getData(); + } + } + } + + return null; + } + + /** + * Simple function to read a file based on a filepath and output it + * + * @param string $filepath + * @return bool|string + */ + public function readFileFunc($filepath) + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + if ($locator->isStream($filepath)) { + $filepath = $locator->findResource($filepath); + } + + if ($filepath && file_exists($filepath)) { + return file_get_contents($filepath); + } + + return false; + } + + /** + * Process a folder as Media and return a media object + * + * @param string $media_dir + * @return Media|null + */ + public function mediaDirFunc($media_dir) + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + if ($locator->isStream($media_dir)) { + $media_dir = $locator->findResource($media_dir); + } + + if ($media_dir && file_exists($media_dir)) { + return new Media($media_dir); + } + + return null; + } + + /** + * Dump a variable to the browser + * + * @param mixed $var + * @return void + */ + public function vardumpFunc($var) + { + dump($var); + } + + /** + * Returns a nicer more readable filesize based on bytes + * + * @param int $bytes + * @return string + */ + public function niceFilesizeFunc($bytes) + { + return Utils::prettySize($bytes); + } + + /** + * Returns a nicer more readable number + * + * @param int|float|string $n + * @return string|bool + */ + public function niceNumberFunc($n) + { + if (!is_float($n) && !is_int($n)) { + if (!is_string($n) || $n === '') { + return false; + } + + // Strip any thousand formatting and find the first number. + $list = array_filter(preg_split("/\D+/", str_replace(',', '', $n))); + $n = reset($list); + + if (!is_numeric($n)) { + return false; + } + + $n = (float)$n; + } + + // now filter it; + if ($n > 1000000000000) { + return round($n/1000000000000, 2).' t'; + } + if ($n > 1000000000) { + return round($n/1000000000, 2).' b'; + } + if ($n > 1000000) { + return round($n/1000000, 2).' m'; + } + if ($n > 1000) { + return round($n/1000, 2).' k'; + } + + return number_format($n); + } + + /** + * Get a theme variable + * Will try to get the variable for the current page, if not found, it tries it's parent page on up to root. + * If still not found, will use the theme's configuration value, + * If still not found, will use the $default value passed in + * + * @param array $context Twig Context + * @param string $var variable to be found (using dot notation) + * @param null $default the default value to be used as last resort + * @param PageInterface|null $page an optional page to use for the current page + * @param bool $exists toggle to simply return the page where the variable is set, else null + * @return mixed + */ + public function themeVarFunc($context, $var, $default = null, $page = null, $exists = false) + { + $page = $page ?? $context['page'] ?? Grav::instance()['page'] ?? null; + + // Try to find var in the page headers + if ($page instanceof PageInterface && $page->exists()) { + // Loop over pages and look for header vars + while ($page && !$page->root()) { + $header = new Data((array)$page->header()); + $value = $header->get($var); + if (isset($value)) { + if ($exists) { + return $page; + } + + return $value; + } + $page = $page->parent(); + } + } + + if ($exists) { + return false; + } + + return Grav::instance()['config']->get('theme.' . $var, $default); + } + + /** + * Look for a page header variable in an array of pages working its way through until a value is found + * + * @param array $context + * @param string $var the variable to look for in the page header + * @param string|string[]|null $pages array of pages to check (current page upwards if not null) + * @return mixed + * @deprecated 1.7 Use themeVarFunc() instead + */ + public function pageHeaderVarFunc($context, $var, $pages = null) + { + if (is_array($pages)) { + $page = array_shift($pages); + } else { + $page = null; + } + return $this->themeVarFunc($context, $var, null, $page); + } + + /** + * takes an array of classes, and if they are not set on body_classes + * look to see if they are set in theme config + * + * @param array $context + * @param string|string[] $classes + * @return string + */ + public function bodyClassFunc($context, $classes) + { + + $header = $context['page']->header(); + $body_classes = $header->body_classes ?? ''; + + foreach ((array)$classes as $class) { + if (!empty($body_classes) && Utils::contains($body_classes, $class)) { + continue; + } + + $val = $this->config->get('theme.' . $class, false) ? $class : false; + $body_classes .= $val ? ' ' . $val : ''; + } + + return $body_classes; + } + + /** + * Returns the content of an SVG image and adds extra classes as needed + * + * @param string $path + * @param string|null $classes + * @return string|string[]|null + */ + public static function svgImageFunction($path, $classes = null, $strip_style = false) + { + $path = Utils::fullPath($path); + + $classes = $classes ?: ''; + + if (file_exists($path) && !is_dir($path)) { + $svg = file_get_contents($path); + $classes = " inline-block $classes"; + $matched = false; + + //Remove xml tag if it exists + $svg = preg_replace('/^<\?xml.*\?>/','', $svg); + + //Strip style if needed + if ($strip_style) { + $svg = preg_replace('//s', '', $svg); + } + + //Look for existing class + $svg = preg_replace_callback('/^]*(class=\"([^"]*)\")[^>]*>/', function($matches) use ($classes, &$matched) { + if (isset($matches[2])) { + $new_classes = $matches[2] . $classes; + $matched = true; + return str_replace($matches[1], "class=\"$new_classes\"", $matches[0]); + } + return $matches[0]; + }, $svg + ); + + // no matches found just add the class + if (!$matched) { + $classes = trim($classes); + $svg = str_replace('jsonSerialize(); + } elseif (method_exists($data, 'toArray')) { + $data = $data->toArray(); + } else { + $data = json_decode(json_encode($data), true); + } + } + + return Yaml::dump($data, $inline); + } + + /** + * Decode/Parse data from YAML format + * + * @param string $data + * @return array + */ + public function yamlDecodeFilter($data) + { + return Yaml::parse($data); + } + + /** + * Function/Filter to return the type of variable + * + * @param mixed $var + * @return string + */ + public function getTypeFunc($var) + { + return gettype($var); + } + + /** + * Function/Filter to test type of variable + * + * @param mixed $var + * @param string|null $typeTest + * @param string|null $className + * @return bool + */ + public function ofTypeFunc($var, $typeTest = null, $className = null) + { + + switch ($typeTest) { + default: + return false; + + case 'array': + return is_array($var); + + case 'bool': + return is_bool($var); + + case 'class': + return is_object($var) === true && get_class($var) === $className; + + case 'float': + return is_float($var); + + case 'int': + return is_int($var); + + case 'numeric': + return is_numeric($var); + + case 'object': + return is_object($var); + + case 'scalar': + return is_scalar($var); + + case 'string': + return is_string($var); + } + } +} diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeCache.php b/system/src/Grav/Common/Twig/Node/TwigNodeCache.php new file mode 100644 index 0000000..0ed1fff --- /dev/null +++ b/system/src/Grav/Common/Twig/Node/TwigNodeCache.php @@ -0,0 +1,58 @@ + $body), array( 'key' => $key, 'lifetime' => $lifetime), $lineno, $tag); + } + + /** + * {@inheritDoc} + */ + public function compile(Compiler $compiler): void + { + $boo = $this->getAttribute('key'); + $compiler + ->addDebugInfo($this) + ->write("\$cache = \\Grav\\Common\\Grav::instance()['cache'];\n") + ->write("\$key = \"twigcache-\" . \"" . $this->getAttribute('key') . "\";\n") + ->write("\$lifetime = " . $this->getAttribute('lifetime') . ";\n") + ->write("\$cache_body = \$cache->fetch(\$key);\n") + ->write("if (\$cache_body === false) {\n") + ->indent() + ->write("ob_start();\n") + ->indent() + ->subcompile($this->getNode('body')) + ->outdent() + ->write("\n") + ->write("\$cache_body = ob_get_clean();\n") + ->write("\$cache->save(\$key, \$cache_body, \$lifetime);\n") + ->outdent() + ->write("}\n") + ->write("echo \$cache_body;\n"); + } +} diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php b/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php new file mode 100644 index 0000000..81cecae --- /dev/null +++ b/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php @@ -0,0 +1,52 @@ + $body], [], $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Compiler $compiler A Twig Compiler instance + * @return void + */ + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write('ob_start();' . PHP_EOL) + ->subcompile($this->getNode('body')) + ->write('$content = ob_get_clean();' . PHP_EOL) + ->write('preg_match("/^\s*/", $content, $matches);' . PHP_EOL) + ->write('$lines = explode("\n", $content);' . PHP_EOL) + ->write('$content = preg_replace(\'/^\' . $matches[0]. \'/\', "", $lines);' . PHP_EOL) + ->write('$content = join("\n", $content);' . PHP_EOL) + ->write('echo $this->env->getExtension(\'Grav\Common\Twig\Extension\GravExtension\')->markdownFunction($context, $content);' . PHP_EOL); + } +} diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeRender.php b/system/src/Grav/Common/Twig/Node/TwigNodeRender.php new file mode 100644 index 0000000..798c6ba --- /dev/null +++ b/system/src/Grav/Common/Twig/Node/TwigNodeRender.php @@ -0,0 +1,83 @@ + $object, 'layout' => $layout, 'context' => $context]; + $nodes = array_filter($nodes); + + parent::__construct($nodes, [], $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Compiler $compiler A Twig Compiler instance + * @return void + * @throws LogicException + */ + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + $compiler->write('$object = ')->subcompile($this->getNode('object'))->raw(';' . PHP_EOL); + + if ($this->hasNode('layout')) { + $layout = $this->getNode('layout'); + $compiler->write('$layout = ')->subcompile($layout)->raw(';' . PHP_EOL); + } else { + $compiler->write('$layout = null;' . PHP_EOL); + } + + if ($this->hasNode('context')) { + $context = $this->getNode('context'); + $compiler->write('$attributes = ')->subcompile($context)->raw(';' . PHP_EOL); + } else { + $compiler->write('$attributes = null;' . PHP_EOL); + } + + $compiler + ->write('$html = $object->render($layout, $attributes ?? []);' . PHP_EOL) + ->write('$block = $context[\'block\'] ?? null;' . PHP_EOL) + ->write('if ($block instanceof \Grav\Framework\ContentBlock\ContentBlock && $html instanceof \Grav\Framework\ContentBlock\ContentBlock) {' . PHP_EOL) + ->indent() + ->write('$block->addBlock($html);' . PHP_EOL) + ->write('echo $html->getToken();' . PHP_EOL) + ->outdent() + ->write('} else {' . PHP_EOL) + ->indent() + ->write('echo (string)$html;' . PHP_EOL) + ->outdent() + ->write('}' . PHP_EOL) + ; + } +} diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeScript.php b/system/src/Grav/Common/Twig/Node/TwigNodeScript.php new file mode 100644 index 0000000..46a0870 --- /dev/null +++ b/system/src/Grav/Common/Twig/Node/TwigNodeScript.php @@ -0,0 +1,104 @@ + $body, 'file' => $file, 'group' => $group, 'priority' => $priority, 'attributes' => $attributes]; + $nodes = array_filter($nodes); + + parent::__construct($nodes, [], $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Compiler $compiler A Twig Compiler instance + * @return void + * @throws LogicException + */ + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + + $compiler->write("\$assets = \\Grav\\Common\\Grav::instance()['assets'];\n"); + + if ($this->hasNode('attributes')) { + $compiler + ->write('$attributes = ') + ->subcompile($this->getNode('attributes')) + ->raw(";\n") + ->write("if (!is_array(\$attributes)) {\n") + ->indent() + ->write("throw new UnexpectedValueException('{% {$this->tagName} with x %}: x is not an array');\n") + ->outdent() + ->write("}\n"); + } else { + $compiler->write('$attributes = [];' . "\n"); + } + + if ($this->hasNode('group')) { + $compiler + ->write("\$attributes['group'] = ") + ->subcompile($this->getNode('group')) + ->raw(";\n") + ->write("if (!is_string(\$attributes['group'])) {\n") + ->indent() + ->write("throw new UnexpectedValueException('{% {$this->tagName} in x %}: x is not a string');\n") + ->outdent() + ->write("}\n"); + } + + if ($this->hasNode('priority')) { + $compiler + ->write("\$attributes['priority'] = (int)(") + ->subcompile($this->getNode('priority')) + ->raw(");\n"); + } + + if ($this->hasNode('file')) { + $compiler + ->write('$assets->addJs(') + ->subcompile($this->getNode('file')) + ->raw(", \$attributes);\n"); + } else { + $compiler + ->write("ob_start();\n") + ->subcompile($this->getNode('body')) + ->write('$content = ob_get_clean();' . "\n") + ->write("\$assets->addInlineJs(\$content, \$attributes);\n"); + } + } +} diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeStyle.php b/system/src/Grav/Common/Twig/Node/TwigNodeStyle.php new file mode 100644 index 0000000..05355f9 --- /dev/null +++ b/system/src/Grav/Common/Twig/Node/TwigNodeStyle.php @@ -0,0 +1,103 @@ + $body, 'file' => $file, 'group' => $group, 'priority' => $priority, 'attributes' => $attributes]; + $nodes = array_filter($nodes); + + parent::__construct($nodes, [], $lineno, $tag); + } + /** + * Compiles the node to PHP. + * + * @param Compiler $compiler A Twig Compiler instance + * @return void + * @throws LogicException + */ + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + + $compiler->write("\$assets = \\Grav\\Common\\Grav::instance()['assets'];\n"); + + if ($this->hasNode('attributes')) { + $compiler + ->write('$attributes = ') + ->subcompile($this->getNode('attributes')) + ->raw(";\n") + ->write("if (!is_array(\$attributes)) {\n") + ->indent() + ->write("throw new UnexpectedValueException('{% {$this->tagName} with x %}: x is not an array');\n") + ->outdent() + ->write("}\n"); + } else { + $compiler->write('$attributes = [];' . "\n"); + } + + if ($this->hasNode('group')) { + $compiler + ->write("\$attributes['group'] = ") + ->subcompile($this->getNode('group')) + ->raw(";\n") + ->write("if (!is_string(\$attributes['group'])) {\n") + ->indent() + ->write("throw new UnexpectedValueException('{% {$this->tagName} in x %}: x is not a string');\n") + ->outdent() + ->write("}\n"); + } + + if ($this->hasNode('priority')) { + $compiler + ->write("\$attributes['priority'] = (int)(") + ->subcompile($this->getNode('priority')) + ->raw(");\n"); + } + + if ($this->hasNode('file')) { + $compiler + ->write('$assets->addCss(') + ->subcompile($this->getNode('file')) + ->raw(", \$attributes);\n"); + } else { + $compiler + ->write("ob_start();\n") + ->subcompile($this->getNode('body')) + ->write('$content = ob_get_clean();' . "\n") + ->write("\$assets->addInlineCss(\$content, \$attributes);\n"); + } + } +} diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php b/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php new file mode 100644 index 0000000..43ae56f --- /dev/null +++ b/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php @@ -0,0 +1,88 @@ + $value, 'cases' => $cases, 'default' => $default]; + $nodes = array_filter($nodes); + + parent::__construct($nodes, [], $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Compiler $compiler A Twig Compiler instance + * @return void + */ + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write('switch (') + ->subcompile($this->getNode('value')) + ->raw(") {\n") + ->indent(); + + /** @var Node $case */ + foreach ($this->getNode('cases') as $case) { + if (!$case->hasNode('body')) { + continue; + } + + foreach ($case->getNode('values') as $value) { + $compiler + ->write('case ') + ->subcompile($value) + ->raw(":\n"); + } + + $compiler + ->write("{\n") + ->indent() + ->subcompile($case->getNode('body')) + ->write("break;\n") + ->outdent() + ->write("}\n"); + } + + if ($this->hasNode('default')) { + $compiler + ->write("default:\n") + ->write("{\n") + ->indent() + ->subcompile($this->getNode('default')) + ->outdent() + ->write("}\n"); + } + + $compiler + ->outdent() + ->write("}\n"); + } +} diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php b/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php new file mode 100644 index 0000000..3bfe612 --- /dev/null +++ b/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php @@ -0,0 +1,52 @@ + $message], ['code' => $code], $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Compiler $compiler A Twig Compiler instance + * @return void + * @throws LogicException + */ + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + + $compiler + ->write('throw new \Grav\Common\Twig\Exception\TwigException(') + ->subcompile($this->getNode('message')) + ->write(', ') + ->write($this->getAttribute('code') ?: 500) + ->write(");\n"); + } +} diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php b/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php new file mode 100644 index 0000000..40e9240 --- /dev/null +++ b/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php @@ -0,0 +1,67 @@ + $try, 'catch' => $catch]; + $nodes = array_filter($nodes); + + parent::__construct($nodes, [], $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Compiler $compiler A Twig Compiler instance + * @return void + * @throws LogicException + */ + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + + $compiler->write('try {'); + + $compiler + ->indent() + ->subcompile($this->getNode('try')) + ->outdent() + ->write('} catch (\Exception $e) {' . "\n") + ->indent() + ->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n") + ->write('$context[\'e\'] = $e;' . "\n"); + + if ($this->hasNode('catch')) { + $compiler->subcompile($this->getNode('catch')); + } + + $compiler + ->outdent() + ->write("}\n"); + } +} diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserCache.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserCache.php new file mode 100644 index 0000000..371e89a --- /dev/null +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserCache.php @@ -0,0 +1,71 @@ +getLine(); + $stream = $this->parser->getStream(); + $key = $this->parser->getVarName() . $lineno; + $lifetime = Grav::instance()['cache']->getLifetime(); + + // Check for optional lifetime override + if (!$stream->test(Token::BLOCK_END_TYPE)) { + $lifetime_expr = $this->parser->getExpressionParser()->parseExpression(); + $lifetime = $lifetime_expr->getAttribute('value'); + } + + $stream->expect(Token::BLOCK_END_TYPE); + $body = $this->parser->subparse(array($this, 'decideCacheEnd'), true); + $stream->expect(Token::BLOCK_END_TYPE); + + return new TwigNodeCache($key, $lifetime, $body, $lineno, $this->getTag()); + } + + /** + * Decide if current token marks end of cache block. + * + * @param Token $token + * @return bool + */ + public function decideCacheEnd(Token $token): bool + { + return $token->test('endcache'); + } + /** + * {@inheritDoc} + */ + public function getTag(): string + { + return 'cache'; + } +} diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserMarkdown.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserMarkdown.php new file mode 100644 index 0000000..9dab4ae --- /dev/null +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserMarkdown.php @@ -0,0 +1,59 @@ +getLine(); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + $body = $this->parser->subparse([$this, 'decideMarkdownEnd'], true); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + return new TwigNodeMarkdown($body, $lineno, $this->getTag()); + } + /** + * Decide if current token marks end of Markdown block. + * + * @param Token $token + * @return bool + */ + public function decideMarkdownEnd(Token $token): bool + { + return $token->test('endmarkdown'); + } + /** + * {@inheritdoc} + */ + public function getTag(): string + { + return 'markdown'; + } +} diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserRender.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserRender.php new file mode 100644 index 0000000..ab2bdde --- /dev/null +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserRender.php @@ -0,0 +1,74 @@ +getLine(); + + [$object, $layout, $context] = $this->parseArguments($token); + + return new TwigNodeRender($object, $layout, $context, $lineno, $this->getTag()); + } + + /** + * @param Token $token + * @return array + */ + protected function parseArguments(Token $token): array + { + $stream = $this->parser->getStream(); + + $object = $this->parser->getExpressionParser()->parseExpression(); + + $layout = null; + if ($stream->nextIf(Token::NAME_TYPE, 'layout')) { + $stream->expect(Token::PUNCTUATION_TYPE, ':'); + $layout = $this->parser->getExpressionParser()->parseExpression(); + } + + $context = null; + if ($stream->nextIf(Token::NAME_TYPE, 'with')) { + $context = $this->parser->getExpressionParser()->parseExpression(); + } + + $stream->expect(Token::BLOCK_END_TYPE); + + return [$object, $layout, $context]; + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag(): string + { + return 'render'; + } +} diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php new file mode 100644 index 0000000..b860631 --- /dev/null +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php @@ -0,0 +1,120 @@ +getLine(); + $stream = $this->parser->getStream(); + + [$file, $group, $priority, $attributes] = $this->parseArguments($token); + + $content = null; + if ($file === null) { + $content = $this->parser->subparse([$this, 'decideBlockEnd'], true); + $stream->expect(Token::BLOCK_END_TYPE); + } + + return new TwigNodeScript($content, $file, $group, $priority, $attributes, $lineno, $this->getTag()); + } + + /** + * @param Token $token + * @return array + */ + protected function parseArguments(Token $token): array + { + $stream = $this->parser->getStream(); + + // Look for deprecated {% script ... in ... %} + if (!$stream->test(Token::BLOCK_END_TYPE) && !$stream->test(Token::OPERATOR_TYPE, 'in')) { + $i = 0; + do { + $token = $stream->look(++$i); + if ($token->test(Token::BLOCK_END_TYPE)) { + break; + } + if ($token->test(Token::OPERATOR_TYPE, 'in') && $stream->look($i+1)->test(Token::STRING_TYPE)) { + user_error("Twig: Using {% script ... in ... %} is deprecated, use {% script ... at ... %} instead", E_USER_DEPRECATED); + + break; + } + } while (true); + } + + $file = null; + if (!$stream->test(Token::NAME_TYPE) && !$stream->test(Token::OPERATOR_TYPE, 'in') && !$stream->test(Token::BLOCK_END_TYPE)) { + $file = $this->parser->getExpressionParser()->parseExpression(); + } + + $group = null; + if ($stream->nextIf(Token::NAME_TYPE, 'at') || $stream->nextIf(Token::OPERATOR_TYPE, 'in')) { + $group = $this->parser->getExpressionParser()->parseExpression(); + } + + $priority = null; + if ($stream->nextIf(Token::NAME_TYPE, 'priority')) { + $stream->expect(Token::PUNCTUATION_TYPE, ':'); + $priority = $this->parser->getExpressionParser()->parseExpression(); + } + + $attributes = null; + if ($stream->nextIf(Token::NAME_TYPE, 'with')) { + $attributes = $this->parser->getExpressionParser()->parseExpression(); + } + + $stream->expect(Token::BLOCK_END_TYPE); + + return [$file, $group, $priority, $attributes]; + } + + /** + * @param Token $token + * @return bool + */ + public function decideBlockEnd(Token $token): bool + { + return $token->test('endscript'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag(): string + { + return 'script'; + } +} diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php new file mode 100644 index 0000000..c8d9544 --- /dev/null +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php @@ -0,0 +1,119 @@ +getLine(); + $stream = $this->parser->getStream(); + + [$file, $group, $priority, $attributes] = $this->parseArguments($token); + + $content = null; + if (!$file) { + $content = $this->parser->subparse([$this, 'decideBlockEnd'], true); + $stream->expect(Token::BLOCK_END_TYPE); + } + + return new TwigNodeStyle($content, $file, $group, $priority, $attributes, $lineno, $this->getTag()); + } + + /** + * @param Token $token + * @return array + */ + protected function parseArguments(Token $token): array + { + $stream = $this->parser->getStream(); + + // Look for deprecated {% style ... in ... %} + if (!$stream->test(Token::BLOCK_END_TYPE) && !$stream->test(Token::OPERATOR_TYPE, 'in')) { + $i = 0; + do { + $token = $stream->look(++$i); + if ($token->test(Token::BLOCK_END_TYPE)) { + break; + } + if ($token->test(Token::OPERATOR_TYPE, 'in') && $stream->look($i+1)->test(Token::STRING_TYPE)) { + user_error("Twig: Using {% style ... in ... %} is deprecated, use {% style ... at ... %} instead", E_USER_DEPRECATED); + + break; + } + } while (true); + } + + $file = null; + if (!$stream->test(Token::NAME_TYPE) && !$stream->test(Token::OPERATOR_TYPE, 'in') && !$stream->test(Token::BLOCK_END_TYPE)) { + $file = $this->parser->getExpressionParser()->parseExpression(); + } + + $group = null; + if ($stream->nextIf(Token::NAME_TYPE, 'at') || $stream->nextIf(Token::OPERATOR_TYPE, 'in')) { + $group = $this->parser->getExpressionParser()->parseExpression(); + } + + $priority = null; + if ($stream->nextIf(Token::NAME_TYPE, 'priority')) { + $stream->expect(Token::PUNCTUATION_TYPE, ':'); + $priority = $this->parser->getExpressionParser()->parseExpression(); + } + + $attributes = null; + if ($stream->nextIf(Token::NAME_TYPE, 'with')) { + $attributes = $this->parser->getExpressionParser()->parseExpression(); + } + + $stream->expect(Token::BLOCK_END_TYPE); + + return [$file, $group, $priority, $attributes]; + } + + /** + * @param Token $token + * @return bool + */ + public function decideBlockEnd(Token $token): bool + { + return $token->test('endstyle'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag(): string + { + return 'style'; + } +} diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php new file mode 100644 index 0000000..4540bbf --- /dev/null +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php @@ -0,0 +1,132 @@ +getLine(); + $stream = $this->parser->getStream(); + + $name = $this->parser->getExpressionParser()->parseExpression(); + $stream->expect(Token::BLOCK_END_TYPE); + + // There can be some whitespace between the {% switch %} and first {% case %} tag. + while ($stream->getCurrent()->getType() === Token::TEXT_TYPE && trim($stream->getCurrent()->getValue()) === '') { + $stream->next(); + } + + $stream->expect(Token::BLOCK_START_TYPE); + + $expressionParser = $this->parser->getExpressionParser(); + + $default = null; + $cases = []; + $end = false; + + while (!$end) { + $next = $stream->next(); + + switch ($next->getValue()) { + case 'case': + $values = []; + + while (true) { + $values[] = $expressionParser->parsePrimaryExpression(); + // Multiple allowed values? + if ($stream->test(Token::OPERATOR_TYPE, 'or')) { + $stream->next(); + } else { + break; + } + } + + $stream->expect(Token::BLOCK_END_TYPE); + $body = $this->parser->subparse([$this, 'decideIfFork']); + $cases[] = new Node([ + 'values' => new Node($values), + 'body' => $body + ]); + break; + + case 'default': + $stream->expect(Token::BLOCK_END_TYPE); + $default = $this->parser->subparse([$this, 'decideIfEnd']); + break; + + case 'endswitch': + $end = true; + break; + + default: + throw new SyntaxError(sprintf('Unexpected end of template. Twig was looking for the following tags "case", "default", or "endswitch" to close the "switch" block started at line %d)', $lineno), -1); + } + } + + $stream->expect(Token::BLOCK_END_TYPE); + + return new TwigNodeSwitch($name, new Node($cases), $default, $lineno, $this->getTag()); + } + + /** + * Decide if current token marks switch logic. + * + * @param Token $token + * @return bool + */ + public function decideIfFork(Token $token): bool + { + return $token->test(['case', 'default', 'endswitch']); + } + + /** + * Decide if current token marks end of swtich block. + * + * @param Token $token + * @return bool + */ + public function decideIfEnd(Token $token): bool + { + return $token->test(['endswitch']); + } + + /** + * {@inheritdoc} + */ + public function getTag(): string + { + return 'switch'; + } +} diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserThrow.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserThrow.php new file mode 100644 index 0000000..bd4adab --- /dev/null +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserThrow.php @@ -0,0 +1,55 @@ + + * {% throw 404 'Not Found' %} + * + */ +class TwigTokenParserThrow extends AbstractTokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Token $token + * @return TwigNodeThrow + * @throws SyntaxError + */ + public function parse(Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + + $code = $stream->expect(Token::NUMBER_TYPE)->getValue(); + $message = $this->parser->getExpressionParser()->parseExpression(); + $stream->expect(Token::BLOCK_END_TYPE); + + return new TwigNodeThrow((int)$code, $message, $lineno, $this->getTag()); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag(): string + { + return 'throw'; + } +} diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php new file mode 100644 index 0000000..46af176 --- /dev/null +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php @@ -0,0 +1,81 @@ + + * {% try %} + *
  • {{ user.get('name') }}
  • + * {% catch %} + * {{ e.message }} + * {% endcatch %} + * + */ +class TwigTokenParserTryCatch extends AbstractTokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Token $token + * @return TwigNodeTryCatch + * @throws SyntaxError + */ + public function parse(Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + + $stream->expect(Token::BLOCK_END_TYPE); + $try = $this->parser->subparse([$this, 'decideCatch']); + $stream->next(); + $stream->expect(Token::BLOCK_END_TYPE); + $catch = $this->parser->subparse([$this, 'decideEnd']); + $stream->next(); + $stream->expect(Token::BLOCK_END_TYPE); + + return new TwigNodeTryCatch($try, $catch, $lineno, $this->getTag()); + } + + /** + * @param Token $token + * @return bool + */ + public function decideCatch(Token $token): bool + { + return $token->test(['catch']); + } + + /** + * @param Token $token + * @return bool + */ + public function decideEnd(Token $token): bool + { + return $token->test(['endtry']) || $token->test(['endcatch']); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag(): string + { + return 'try'; + } +} diff --git a/system/src/Grav/Common/Twig/Twig.php b/system/src/Grav/Common/Twig/Twig.php new file mode 100644 index 0000000..19bad01 --- /dev/null +++ b/system/src/Grav/Common/Twig/Twig.php @@ -0,0 +1,553 @@ +grav = $grav; + $this->twig_paths = []; + } + + /** + * Twig initialization that sets the twig loader chain, then the environment, then extensions + * and also the base set of twig vars + * + * @return $this + */ + public function init() + { + if (null === $this->twig) { + /** @var Config $config */ + $config = $this->grav['config']; + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + /** @var Language $language */ + $language = $this->grav['language']; + + $active_language = $language->getActive(); + + // handle language templates if available + if ($language->enabled()) { + $lang_templates = $locator->findResource('theme://templates/' . ($active_language ?: $language->getDefault())); + if ($lang_templates) { + $this->twig_paths[] = $lang_templates; + } + } + + $this->twig_paths = array_merge($this->twig_paths, $locator->findResources('theme://templates')); + + $this->grav->fireEvent('onTwigTemplatePaths'); + + // Add Grav core templates location + $core_templates = array_merge($locator->findResources('system://templates'), $locator->findResources('system://templates/testing')); + $this->twig_paths = array_merge($this->twig_paths, $core_templates); + + $this->loader = new FilesystemLoader($this->twig_paths); + + // Register all other prefixes as namespaces in twig + foreach ($locator->getPaths('theme') as $prefix => $_) { + if ($prefix === '') { + continue; + } + + $twig_paths = []; + + // handle language templates if available + if ($language->enabled()) { + $lang_templates = $locator->findResource('theme://'.$prefix.'templates/' . ($active_language ?: $language->getDefault())); + if ($lang_templates) { + $twig_paths[] = $lang_templates; + } + } + + $twig_paths = array_merge($twig_paths, $locator->findResources('theme://'.$prefix.'templates')); + + $namespace = trim($prefix, '/'); + $this->loader->setPaths($twig_paths, $namespace); + } + + $this->grav->fireEvent('onTwigLoader'); + + $this->loaderArray = new ArrayLoader([]); + $loader_chain = new ChainLoader([$this->loaderArray, $this->loader]); + + $params = $config->get('system.twig'); + if (!empty($params['cache'])) { + $cachePath = $locator->findResource('cache://twig', true, true); + $params['cache'] = new FilesystemCache($cachePath, FilesystemCache::FORCE_BYTECODE_INVALIDATION); + } + + if (!$config->get('system.strict_mode.twig_compat', false)) { + // Force autoescape on for all files if in strict mode. + $params['autoescape'] = 'html'; + } elseif (!empty($this->autoescape)) { + $params['autoescape'] = $this->autoescape ? 'html' : false; + } + + if (empty($params['autoescape'])) { + user_error('Grav 2.0 will have Twig auto-escaping forced on (can be emulated by turning off \'system.strict_mode.twig_compat\' setting in your configuration)', E_USER_DEPRECATED); + } + + $this->twig = new TwigEnvironment($loader_chain, $params); + + $this->twig->registerUndefinedFunctionCallback(function ($name) use ($config) { + $allowed = $config->get('system.twig.safe_functions'); + if (is_array($allowed) && in_array($name, $allowed, true) && function_exists($name)) { + return new TwigFunction($name, $name); + } + if ($config->get('system.twig.undefined_functions')) { + if (function_exists($name)) { + if (!Utils::isDangerousFunction($name)) { + user_error("PHP function {$name}() was used as Twig function. This is deprecated in Grav 1.7. Please add it to system configuration: `system.twig.safe_functions`", E_USER_DEPRECATED); + + return new TwigFunction($name, $name); + } + + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addException(new RuntimeException("Blocked potentially dangerous PHP function {$name}() being used as Twig function. If you really want to use it, please add it to system configuration: `system.twig.safe_functions`")); + } + + return new TwigFunction($name, static function () {}); + } + + return false; + }); + + $this->twig->registerUndefinedFilterCallback(function ($name) use ($config) { + $allowed = $config->get('system.twig.safe_filters'); + if (is_array($allowed) && in_array($name, $allowed, true) && function_exists($name)) { + return new TwigFilter($name, $name); + } + if ($config->get('system.twig.undefined_filters')) { + if (function_exists($name)) { + if (!Utils::isDangerousFunction($name)) { + user_error("PHP function {$name}() used as Twig filter. This is deprecated in Grav 1.7. Please add it to system configuration: `system.twig.safe_filters`", E_USER_DEPRECATED); + + return new TwigFilter($name, $name); + } + + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addException(new RuntimeException("Blocked potentially dangerous PHP function {$name}() being used as Twig filter. If you really want to use it, please add it to system configuration: `system.twig.safe_filters`")); + } + + return new TwigFilter($name, static function () {}); + } + + return false; + }); + + $this->grav->fireEvent('onTwigInitialized'); + + // set default date format if set in config + if ($config->get('system.pages.dateformat.long')) { + /** @var CoreExtension $extension */ + $extension = $this->twig->getExtension(CoreExtension::class); + $extension->setDateFormat($config->get('system.pages.dateformat.long')); + } + // enable the debug extension if required + if ($config->get('system.twig.debug')) { + $this->twig->addExtension(new DebugExtension()); + } + $this->twig->addExtension(new GravExtension()); + $this->twig->addExtension(new FilesystemExtension()); + $this->twig->addExtension(new DeferredExtension()); + $this->twig->addExtension(new StringLoaderExtension()); + + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addTwigProfiler($this->twig); + + $this->grav->fireEvent('onTwigExtensions'); + + /** @var Pages $pages */ + $pages = $this->grav['pages']; + + // Set some standard variables for twig + $this->twig_vars += [ + 'config' => $config, + 'system' => $config->get('system'), + 'theme' => $config->get('theme'), + 'site' => $config->get('site'), + 'uri' => $this->grav['uri'], + 'assets' => $this->grav['assets'], + 'taxonomy' => $this->grav['taxonomy'], + 'browser' => $this->grav['browser'], + 'base_dir' => GRAV_ROOT, + 'home_url' => $pages->homeUrl($active_language), + 'base_url' => $pages->baseUrl($active_language), + 'base_url_absolute' => $pages->baseUrl($active_language, true), + 'base_url_relative' => $pages->baseUrl($active_language, false), + 'base_url_simple' => $this->grav['base_url'], + 'theme_dir' => $locator->findResource('theme://'), + 'theme_url' => $this->grav['base_url'] . '/' . $locator->findResource('theme://', false), + 'html_lang' => $this->grav['language']->getActive() ?: $config->get('site.default_lang', 'en'), + 'language_codes' => new LanguageCodes, + ]; + } + + return $this; + } + + /** + * @return Environment + */ + public function twig() + { + return $this->twig; + } + + /** + * @return FilesystemLoader + */ + public function loader() + { + return $this->loader; + } + + /** + * @return Profile + */ + public function profile() + { + return $this->profile; + } + + + /** + * Adds or overrides a template. + * + * @param string $name The template name + * @param string $template The template source + */ + public function setTemplate($name, $template) + { + $this->loaderArray->setTemplate($name, $template); + } + + /** + * Twig process that renders a page item. It supports two variations: + * 1) Handles modular pages by rendering a specific page based on its modular twig template + * 2) Renders individual page items for twig processing before the site rendering + * + * @param PageInterface $item The page item to render + * @param string|null $content Optional content override + * + * @return string The rendered output + */ + public function processPage(PageInterface $item, $content = null) + { + $content = $content ?? $item->content(); + + // override the twig header vars for local resolution + $this->grav->fireEvent('onTwigPageVariables', new Event(['page' => $item])); + $twig_vars = $this->twig_vars; + + $twig_vars['page'] = $item; + $twig_vars['media'] = $item->media(); + $twig_vars['header'] = $item->header(); + $local_twig = clone $this->twig; + + $output = ''; + + try { + if ($item->isModule()) { + $twig_vars['content'] = $content; + $template = $this->getPageTwigTemplate($item); + $output = $content = $local_twig->render($template, $twig_vars); + } + + // Process in-page Twig + if ($item->shouldProcess('twig')) { + $name = '@Page:' . $item->path(); + $this->setTemplate($name, $content); + $output = $local_twig->render($name, $twig_vars); + } + + } catch (LoaderError $e) { + throw new RuntimeException($e->getRawMessage(), 400, $e); + } + + return $output; + } + + /** + * Process a Twig template directly by using a template name + * and optional array of variables + * + * @param string $template template to render with + * @param array $vars Optional variables + * + * @return string + */ + public function processTemplate($template, $vars = []) + { + // override the twig header vars for local resolution + $this->grav->fireEvent('onTwigTemplateVariables'); + $vars += $this->twig_vars; + + try { + $output = $this->twig->render($template, $vars); + } catch (LoaderError $e) { + throw new RuntimeException($e->getRawMessage(), 404, $e); + } + + return $output; + } + + + /** + * Process a Twig template directly by using a Twig string + * and optional array of variables + * + * @param string $string string to render. + * @param array $vars Optional variables + * + * @return string + */ + public function processString($string, array $vars = []) + { + // override the twig header vars for local resolution + $this->grav->fireEvent('onTwigStringVariables'); + $vars += $this->twig_vars; + + $name = '@Var:' . $string; + $this->setTemplate($name, $string); + + try { + $output = $this->twig->render($name, $vars); + } catch (LoaderError $e) { + throw new RuntimeException($e->getRawMessage(), 404, $e); + } + + return $output; + } + + /** + * Twig process that renders the site layout. This is the main twig process that renders the overall + * page and handles all the layout for the site display. + * + * @param string|null $format Output format (defaults to HTML). + * @param array $vars + * @return string the rendered output + * @throws RuntimeException + */ + public function processSite($format = null, array $vars = []) + { + try { + $grav = $this->grav; + + // set the page now its been processed + $grav->fireEvent('onTwigSiteVariables'); + + /** @var Pages $pages */ + $pages = $grav['pages']; + + /** @var PageInterface $page */ + $page = $grav['page']; + + $twig_vars = $this->twig_vars; + $twig_vars['theme'] = $grav['config']->get('theme'); + $twig_vars['pages'] = $pages->root(); + $twig_vars['page'] = $page; + $twig_vars['header'] = $page->header(); + $twig_vars['media'] = $page->media(); + $twig_vars['content'] = $page->content(); + + // determine if params are set, if so disable twig cache + $params = $grav['uri']->params(null, true); + if (!empty($params)) { + $this->twig->setCache(false); + } + + // Get Twig template layout + $template = $this->getPageTwigTemplate($page, $format); + $page->templateFormat($format); + + $output = $this->twig->render($template, $vars + $twig_vars); + } catch (LoaderError $e) { + throw new RuntimeException($e->getMessage(), 400, $e); + } catch (RuntimeError $e) { + $prev = $e->getPrevious(); + if ($prev instanceof TwigException) { + $code = $prev->getCode() ?: 500; + // Fire onPageNotFound event. + $event = new Event([ + 'page' => $page, + 'code' => $code, + 'message' => $prev->getMessage(), + 'exception' => $prev, + 'route' => $grav['route'], + 'request' => $grav['request'] + ]); + $event = $grav->fireEvent("onDisplayErrorPage.{$code}", $event); + $newPage = $event['page']; + if ($newPage && $newPage !== $page) { + unset($grav['page']); + $grav['page'] = $newPage; + + return $this->processSite($newPage->templateFormat(), $vars); + } + } + + throw $e; + } + + return $output; + } + + /** + * Wraps the FilesystemLoader addPath method (should be used only in `onTwigLoader()` event + * @param string $template_path + * @param string $namespace + * @throws LoaderError + */ + public function addPath($template_path, $namespace = '__main__') + { + $this->loader->addPath($template_path, $namespace); + } + + /** + * Wraps the FilesystemLoader prependPath method (should be used only in `onTwigLoader()` event + * @param string $template_path + * @param string $namespace + * @throws LoaderError + */ + public function prependPath($template_path, $namespace = '__main__') + { + $this->loader->prependPath($template_path, $namespace); + } + + /** + * Simple helper method to get the twig template if it has already been set, else return + * the one being passed in + * + * @param string $template the template name + * @return string the template name + */ + public function template($template) + { + return $this->template ?? $template; + } + + /** + * @param PageInterface $page + * @param string|null $format + * @return string + */ + public function getPageTwigTemplate($page, &$format = null) + { + $template = $page->template(); + $default = $page->isModule() ? 'modular/default' : 'default'; + $extension = $format ?: $page->templateFormat(); + $twig_extension = $extension ? '.'. $extension .TWIG_EXT : TEMPLATE_EXT; + $template_file = $this->template($page->template() . $twig_extension); + + $loader = $this->twig->getLoader(); + if ($loader->exists($template_file)) { + // template.xxx.twig + $page_template = $template_file; + } elseif ($twig_extension !== TEMPLATE_EXT && $loader->exists($template . TEMPLATE_EXT)) { + // template.html.twig + $page_template = $template . TEMPLATE_EXT; + $format = 'html'; + } elseif ($loader->exists($default . $twig_extension)) { + // default.xxx.twig + $page_template = $default . $twig_extension; + } else { + // default.html.twig + $page_template = $default . TEMPLATE_EXT; + $format = 'html'; + } + + return $page_template; + + } + + /** + * Overrides the autoescape setting + * + * @param bool $state + * @return void + * @deprecated 1.5 Auto-escape should always be turned on to protect against XSS issues (can be disabled per template file). + */ + public function setAutoescape($state) + { + if (!$state) { + user_error(__CLASS__ . '::' . __FUNCTION__ . '(false) is deprecated since Grav 1.5', E_USER_DEPRECATED); + } + + $this->autoescape = (bool) $state; + } +} diff --git a/system/src/Grav/Common/Twig/TwigClockworkDataSource.php b/system/src/Grav/Common/Twig/TwigClockworkDataSource.php new file mode 100644 index 0000000..11127b8 --- /dev/null +++ b/system/src/Grav/Common/Twig/TwigClockworkDataSource.php @@ -0,0 +1,58 @@ +twig = $twig; + } + + /** + * Register the Twig profiler extension + */ + public function listenToEvents(): void + { + $this->twig->addExtension(new ProfilerExtension($this->profile = new Profile())); + } + + /** + * Adds rendered views to the request + * + * @param Request $request + * @return Request + */ + public function resolve(Request $request) + { + $timeline = (new TwigClockworkDumper())->dump($this->profile); + + $request->viewsData = array_merge($request->viewsData, $timeline->finalize()); + + return $request; + } +} diff --git a/system/src/Grav/Common/Twig/TwigClockworkDumper.php b/system/src/Grav/Common/Twig/TwigClockworkDumper.php new file mode 100644 index 0000000..2c1f4be --- /dev/null +++ b/system/src/Grav/Common/Twig/TwigClockworkDumper.php @@ -0,0 +1,72 @@ +dumpProfile($profile, $timeline); + + return $timeline; + } + + /** + * @param Profile $profile + * @param Timeline $timeline + * @param null $parent + */ + public function dumpProfile(Profile $profile, Timeline $timeline, $parent = null) + { + $id = $this->lastId++; + + if ($profile->isRoot()) { + $name = $profile->getName(); + } elseif ($profile->isTemplate()) { + $name = $profile->getTemplate(); + } else { + $name = $profile->getTemplate() . '::' . $profile->getType() . '(' . $profile->getName() . ')'; + } + + foreach ($profile as $p) { + $this->dumpProfile($p, $timeline, $id); + } + + $data = $profile->__serialize(); + + $timeline->event($name, [ + 'name' => $id, + 'start' => $data[3]['wt'] ?? null, + 'end' => $data[4]['wt'] ?? null, + 'data' => [ + 'data' => [], + 'memoryUsage' => $data[4]['mu'] ?? null, + 'parent' => $parent + ] + ]); + } +} diff --git a/system/src/Grav/Common/Twig/TwigEnvironment.php b/system/src/Grav/Common/Twig/TwigEnvironment.php new file mode 100644 index 0000000..bebbdf1 --- /dev/null +++ b/system/src/Grav/Common/Twig/TwigEnvironment.php @@ -0,0 +1,21 @@ +get('system.twig.umask_fix', false); + } + + if (self::$umask) { + $dir = dirname($file); + if (!is_dir($dir)) { + $old = umask(0002); + Folder::create($dir); + umask($old); + } + parent::writeCacheFile($file, $content); + chmod($file, 0775); + } else { + parent::writeCacheFile($file, $content); + } + } +} diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php new file mode 100644 index 0000000..7bef097 --- /dev/null +++ b/system/src/Grav/Common/Uri.php @@ -0,0 +1,1524 @@ +createFromString($env); + } else { + $this->createFromEnvironment(is_array($env) ? $env : $_SERVER); + } + } + + /** + * Initialize the URI class with a url passed via parameter. + * Used for testing purposes. + * + * @param string $url the URL to use in the class + * @return $this + */ + public function initializeWithUrl($url = '') + { + if ($url) { + $this->createFromString($url); + } + + return $this; + } + + /** + * Initialize the URI class by providing url and root_path arguments + * + * @param string $url + * @param string $root_path + * @return $this + */ + public function initializeWithUrlAndRootPath($url, $root_path) + { + $this->initializeWithUrl($url); + $this->root_path = $root_path; + + return $this; + } + + /** + * Validate a hostname + * + * @param string $hostname The hostname + * @return bool + */ + public function validateHostname($hostname) + { + return (bool)preg_match(static::HOSTNAME_REGEX, $hostname); + } + + /** + * Initializes the URI object based on the url set on the object + * + * @return void + */ + public function init() + { + $grav = Grav::instance(); + + /** @var Config $config */ + $config = $grav['config']; + + /** @var Language $language */ + $language = $grav['language']; + + // add the port to the base for non-standard ports + if ($this->port && $config->get('system.reverse_proxy_setup') === false) { + $this->base .= ':' . $this->port; + } + + // Handle custom base + $custom_base = rtrim($grav['config']->get('system.custom_base_url'), '/'); + if ($custom_base) { + $custom_parts = parse_url($custom_base); + if ($custom_parts === false) { + throw new RuntimeException('Bad configuration: system.custom_base_url'); + } + $orig_root_path = $this->root_path; + $this->root_path = isset($custom_parts['path']) ? rtrim($custom_parts['path'], '/') : ''; + if (isset($custom_parts['scheme'])) { + $this->base = $custom_parts['scheme'] . '://' . $custom_parts['host']; + $this->port = $custom_parts['port'] ?? null; + if ($this->port && $config->get('system.reverse_proxy_setup') === false) { + $this->base .= ':' . $this->port; + } + $this->root = $custom_base; + } else { + $this->root = $this->base . $this->root_path; + } + $this->uri = Utils::replaceFirstOccurrence($orig_root_path, $this->root_path, $this->uri); + } else { + $this->root = $this->base . $this->root_path; + } + + $this->url = $this->base . $this->uri; + + $uri = Utils::replaceFirstOccurrence(static::filterPath($this->root), '', $this->url); + + // remove the setup.php based base if set: + $setup_base = $grav['pages']->base(); + if ($setup_base) { + $uri = preg_replace('|^' . preg_quote($setup_base, '|') . '|', '', $uri); + } + $this->setup_base = $setup_base; + + // process params + $uri = $this->processParams($uri, $config->get('system.param_sep')); + + // set active language + $uri = $language->setActiveFromUri($uri); + + // split the URL and params (and make sure that the path isn't seen as domain) + $bits = parse_url('http://domain.com' . $uri); + + //process fragment + if (isset($bits['fragment'])) { + $this->fragment = $bits['fragment']; + } + + // Get the path. If there's no path, make sure pathinfo() still returns dirname variable + $path = $bits['path'] ?? '/'; + + // remove the extension if there is one set + $parts = pathinfo($path); + + // set the original basename + $this->basename = $parts['basename']; + + // set the extension + if (isset($parts['extension'])) { + $this->extension = $parts['extension']; + } + + // Strip the file extension for valid page types + if ($this->isValidExtension($this->extension)) { + $path = Utils::replaceLastOccurrence(".{$this->extension}", '', $path); + } + + // set the new url + $this->url = $this->root . $path; + $this->path = static::cleanPath($path); + $this->content_path = trim(Utils::replaceFirstOccurrence($this->base, '', $this->path), '/'); + if ($this->content_path !== '') { + $this->paths = explode('/', $this->content_path); + } + + // Set some Grav stuff + $grav['base_url_absolute'] = $config->get('system.custom_base_url') ?: $this->rootUrl(true); + $grav['base_url_relative'] = $this->rootUrl(false); + $grav['base_url'] = $config->get('system.absolute_urls') ? $grav['base_url_absolute'] : $grav['base_url_relative']; + + RouteFactory::setRoot($this->root_path . $setup_base); + RouteFactory::setLanguage($language->getLanguageURLPrefix()); + RouteFactory::setParamValueDelimiter($config->get('system.param_sep')); + } + + /** + * Return URI path. + * + * @param int|null $id + * @return string|string[] + */ + public function paths($id = null) + { + if ($id !== null) { + return $this->paths[$id]; + } + + return $this->paths; + } + + /** + * Return route to the current URI. By default route doesn't include base path. + * + * @param bool $absolute True to include full path. + * @param bool $domain True to include domain. Works only if first parameter is also true. + * @return string + */ + public function route($absolute = false, $domain = false) + { + return ($absolute ? $this->rootUrl($domain) : '') . '/' . implode('/', $this->paths); + } + + /** + * Return full query string or a single query attribute. + * + * @param string|null $id Optional attribute. Get a single query attribute if set + * @param bool $raw If true and $id is not set, return the full query array. Otherwise return the query string + * + * @return string|array Returns an array if $id = null and $raw = true + */ + public function query($id = null, $raw = false) + { + if ($id !== null) { + return $this->queries[$id] ?? null; + } + + if ($raw) { + return $this->queries; + } + + if (!$this->queries) { + return ''; + } + + return http_build_query($this->queries); + } + + /** + * Return all or a single query parameter as a URI compatible string. + * + * @param string|null $id Optional parameter name. + * @param boolean $array return the array format or not + * @return null|string|array + */ + public function params($id = null, $array = false) + { + $config = Grav::instance()['config']; + $sep = $config->get('system.param_sep'); + + $params = null; + if ($id === null) { + if ($array) { + return $this->params; + } + $output = []; + foreach ($this->params as $key => $value) { + $output[] = "{$key}{$sep}{$value}"; + $params = '/' . implode('/', $output); + } + } elseif (isset($this->params[$id])) { + if ($array) { + return $this->params[$id]; + } + $params = "/{$id}{$sep}{$this->params[$id]}"; + } + + return $params; + } + + /** + * Get URI parameter. + * + * @param string|null $id + * @param string|bool|null $default + * @return bool|string + */ + public function param($id, $default = false) + { + if (isset($this->params[$id])) { + return html_entity_decode(rawurldecode($this->params[$id]), ENT_COMPAT | ENT_HTML401, 'UTF-8'); + } + + return $default; + } + + /** + * Gets the Fragment portion of a URI (eg #target) + * + * @param string|null $fragment + * @return string|null + */ + public function fragment($fragment = null) + { + if ($fragment !== null) { + $this->fragment = $fragment; + } + return $this->fragment; + } + + /** + * Return URL. + * + * @param bool $include_host Include hostname. + * @return string + */ + public function url($include_host = false) + { + if ($include_host) { + return $this->url; + } + + $url = Utils::replaceFirstOccurrence($this->base, '', rtrim($this->url, '/')); + + return $url ?: '/'; + } + + /** + * Return the Path + * + * @return string The path of the URI + */ + public function path() + { + return $this->path; + } + + /** + * Return the Extension of the URI + * + * @param string|null $default + * @return string|null The extension of the URI + */ + public function extension($default = null) + { + if (!$this->extension) { + $this->extension = $default; + } + + return $this->extension; + } + + /** + * @return string + */ + public function method() + { + $method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; + + if ($method === 'POST' && isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { + $method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); + } + + return $method; + } + + /** + * Return the scheme of the URI + * + * @param bool|null $raw + * @return string The scheme of the URI + */ + public function scheme($raw = false) + { + if (!$raw) { + $scheme = ''; + if ($this->scheme) { + $scheme = $this->scheme . '://'; + } elseif ($this->host) { + $scheme = '//'; + } + + return $scheme; + } + + return $this->scheme; + } + + + /** + * Return the host of the URI + * + * @return string|null The host of the URI + */ + public function host() + { + return $this->host; + } + + /** + * Return the port number if it can be figured out + * + * @param bool $raw + * @return int|null + */ + public function port($raw = false) + { + $port = $this->port; + // If not in raw mode and port is not set or is 0, figure it out from scheme. + if (!$raw && !$port) { + if ($this->scheme === 'http') { + $this->port = 80; + } elseif ($this->scheme === 'https') { + $this->port = 443; + } + } + + return $this->port ?: null; + } + + /** + * Return user + * + * @return string|null + */ + public function user() + { + return $this->user; + } + + /** + * Return password + * + * @return string|null + */ + public function password() + { + return $this->password; + } + + /** + * Gets the environment name + * + * @return string + */ + public function environment() + { + return $this->env; + } + + + /** + * Return the basename of the URI + * + * @return string The basename of the URI + */ + public function basename() + { + return $this->basename; + } + + /** + * Return the full uri + * + * @param bool $include_root + * @return string + */ + public function uri($include_root = true) + { + if ($include_root) { + return $this->uri; + } + + return Utils::replaceFirstOccurrence($this->root_path, '', $this->uri); + } + + /** + * Return the base of the URI + * + * @return string The base of the URI + */ + public function base() + { + return $this->base; + } + + /** + * Return the base relative URL including the language prefix + * or the base relative url if multi-language is not enabled + * + * @return string The base of the URI + */ + public function baseIncludingLanguage() + { + $grav = Grav::instance(); + + /** @var Pages $pages */ + $pages = $grav['pages']; + + return $pages->baseUrl(null, false); + } + + /** + * Return root URL to the site. + * + * @param bool $include_host Include hostname. + * @return string + */ + public function rootUrl($include_host = false) + { + if ($include_host) { + return $this->root; + } + + return Utils::replaceFirstOccurrence($this->base, '', $this->root); + } + + /** + * Return current page number. + * + * @return int + */ + public function currentPage() + { + $page = (int)($this->params['page'] ?? 1); + + return max(1, $page); + } + + /** + * Return relative path to the referrer defaulting to current or given page. + * + * You should set the third parameter to `true` for redirects as long as you came from the same sub-site and language. + * + * @param string|null $default + * @param string|null $attributes + * @param bool $withoutBaseRoute + * @return string + */ + public function referrer($default = null, $attributes = null, bool $withoutBaseRoute = false) + { + $referrer = $_SERVER['HTTP_REFERER'] ?? null; + + // Check that referrer came from our site. + if ($withoutBaseRoute) { + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + $base = $pages->baseUrl(null, true); + } else { + $base = $this->rootUrl(true); + } + + // Referrer should always have host set and it should come from the same base address. + if (!is_string($referrer) || !str_starts_with($referrer, $base)) { + $referrer = $default ?: $this->route(true, true); + } + + // Relative path from grav root. + $referrer = substr($referrer, strlen($base)); + if ($attributes) { + $referrer .= $attributes; + } + + return $referrer; + } + + /** + * @return string + */ + public function __toString() + { + return static::buildUrl($this->toArray()); + } + + /** + * @return string + */ + public function toOriginalString() + { + return static::buildUrl($this->toArray(true)); + } + + /** + * @param bool $full + * @return array + */ + public function toArray($full = false) + { + if ($full === true) { + $root_path = $this->root_path ?? ''; + $extension = isset($this->extension) && $this->isValidExtension($this->extension) ? '.' . $this->extension : ''; + $path = $root_path . $this->path . $extension; + } else { + $path = $this->path; + } + + return [ + 'scheme' => $this->scheme, + 'host' => $this->host, + 'port' => $this->port ?: null, + 'user' => $this->user, + 'pass' => $this->password, + 'path' => $path, + 'params' => $this->params, + 'query' => $this->query, + 'fragment' => $this->fragment + ]; + } + + /** + * Calculate the parameter regex based on the param_sep setting + * + * @return string + */ + public static function paramsRegex() + { + return '/\/{1,}([^\:\#\/\?]*' . Grav::instance()['config']->get('system.param_sep') . '[^\:\#\/\?]*)/'; + } + + /** + * Return the IP address of the current user + * + * @return string ip address + */ + public static function ip() + { + $ip = 'UNKNOWN'; + + if (getenv('HTTP_CLIENT_IP')) { + $ip = getenv('HTTP_CLIENT_IP'); + } elseif (getenv('HTTP_CF_CONNECTING_IP')) { + $ip = getenv('HTTP_CF_CONNECTING_IP'); + } elseif (getenv('HTTP_X_FORWARDED_FOR') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) { + $ips = array_map('trim', explode(',', getenv('HTTP_X_FORWARDED_FOR'))); + $ip = array_shift($ips); + } elseif (getenv('HTTP_X_FORWARDED') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) { + $ip = getenv('HTTP_X_FORWARDED'); + } elseif (getenv('HTTP_FORWARDED_FOR')) { + $ip = getenv('HTTP_FORWARDED_FOR'); + } elseif (getenv('HTTP_FORWARDED')) { + $ip = getenv('HTTP_FORWARDED'); + } elseif (getenv('REMOTE_ADDR')) { + $ip = getenv('REMOTE_ADDR'); + } + + return $ip; + } + + /** + * Returns current Uri. + * + * @return \Grav\Framework\Uri\Uri + */ + public static function getCurrentUri() + { + if (!static::$currentUri) { + static::$currentUri = UriFactory::createFromEnvironment($_SERVER); + } + + return static::$currentUri; + } + + /** + * Returns current route. + * + * @return Route + */ + public static function getCurrentRoute() + { + if (!static::$currentRoute) { + /** @var Uri $uri */ + $uri = Grav::instance()['uri']; + + static::$currentRoute = RouteFactory::createFromLegacyUri($uri); + } + + return static::$currentRoute; + } + + /** + * Is this an external URL? if it starts with `http` then yes, else false + * + * @param string $url the URL in question + * @return bool is eternal state + */ + public static function isExternal($url) + { + return (0 === strpos($url, 'http://') || 0 === strpos($url, 'https://') || 0 === strpos($url, '//')); + } + + /** + * The opposite of built-in PHP method parse_url() + * + * @param array $parsed_url + * @return string + */ + public static function buildUrl($parsed_url) + { + $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . ':' : ''; + $authority = isset($parsed_url['host']) ? '//' : ''; + $host = $parsed_url['host'] ?? ''; + $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; + $user = $parsed_url['user'] ?? ''; + $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; + $pass = ($user || $pass) ? "{$pass}@" : ''; + $path = $parsed_url['path'] ?? ''; + $path = !empty($parsed_url['params']) ? rtrim($path, '/') . static::buildParams($parsed_url['params']) : $path; + $query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; + $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; + + return "{$scheme}{$authority}{$user}{$pass}{$host}{$port}{$path}{$query}{$fragment}"; + } + + /** + * @param array $params + * @return string + */ + public static function buildParams(array $params) + { + if (!$params) { + return ''; + } + + $grav = Grav::instance(); + $sep = $grav['config']->get('system.param_sep'); + + $output = []; + foreach ($params as $key => $value) { + $output[] = "{$key}{$sep}{$value}"; + } + + return '/' . implode('/', $output); + } + + /** + * Converts links from absolute '/' or relative (../..) to a Grav friendly format + * + * @param PageInterface $page the current page to use as reference + * @param string|array $url the URL as it was written in the markdown + * @param string $type the type of URL, image | link + * @param bool $absolute if null, will use system default, if true will use absolute links internally + * @param bool $route_only only return the route, not full URL path + * @return string|array the more friendly formatted url + */ + public static function convertUrl(PageInterface $page, $url, $type = 'link', $absolute = false, $route_only = false) + { + $grav = Grav::instance(); + + $uri = $grav['uri']; + + // Link processing should prepend language + $language = $grav['language']; + $language_append = ''; + if ($type === 'link' && $language->enabled()) { + $language_append = $language->getLanguageURLPrefix(); + } + + // Handle Excerpt style $url array + $url_path = is_array($url) ? $url['path'] : $url; + + $external = false; + $base = $grav['base_url_relative']; + $base_url = rtrim($base . $grav['pages']->base(), '/') . $language_append; + $pages_dir = $grav['locator']->findResource('page://'); + + // if absolute and starts with a base_url move on + if (isset($url['scheme']) && Utils::startsWith($url['scheme'], 'http')) { + $external = true; + } elseif ($url_path === '' && isset($url['fragment'])) { + $external = true; + } elseif ($url_path === '/' || ($base_url !== '' && Utils::startsWith($url_path, $base_url))) { + $url_path = $base_url . $url_path; + } else { + // see if page is relative to this or absolute + if (Utils::startsWith($url_path, '/')) { + $normalized_url = Utils::normalizePath($base_url . $url_path); + $normalized_path = Utils::normalizePath($pages_dir . $url_path); + } else { + $page_route = ($page->home() && !empty($url_path)) ? $page->rawRoute() : $page->route(); + $normalized_url = $base_url . Utils::normalizePath(rtrim($page_route, '/') . '/' . $url_path); + $normalized_path = Utils::normalizePath($page->path() . '/' . $url_path); + } + + // special check to see if path checking is required. + $just_path = Utils::replaceFirstOccurrence($normalized_url, '', $normalized_path); + if ($normalized_url === '/' || $just_path === $page->path()) { + $url_path = $normalized_url; + } else { + $url_bits = static::parseUrl($normalized_path); + $full_path = $url_bits['path']; + $raw_full_path = rawurldecode($full_path); + + if (file_exists($raw_full_path)) { + $full_path = $raw_full_path; + } elseif (!file_exists($full_path)) { + $full_path = false; + } + + if ($full_path) { + $path_info = pathinfo($full_path); + $page_path = $path_info['dirname']; + $filename = ''; + + if ($url_path === '..') { + $page_path = $full_path; + } else { + // save the filename if a file is part of the path + if (is_file($full_path)) { + if ($path_info['extension'] !== 'md') { + $filename = '/' . $path_info['basename']; + } + } else { + $page_path = $full_path; + } + } + + // get page instances and try to find one that fits + $instances = $grav['pages']->instances(); + if (isset($instances[$page_path])) { + /** @var PageInterface $target */ + $target = $instances[$page_path]; + $url_bits['path'] = $base_url . rtrim($target->route(), '/') . $filename; + + $url_path = Uri::buildUrl($url_bits); + } else { + $url_path = $normalized_url; + } + } else { + $url_path = $normalized_url; + } + } + } + + // handle absolute URLs + if (is_array($url) && !$external && ($absolute === true || $grav['config']->get('system.absolute_urls', false))) { + $url['scheme'] = $uri->scheme(true); + $url['host'] = $uri->host(); + $url['port'] = $uri->port(true); + + // check if page exists for this route, and if so, check if it has SSL enabled + $pages = $grav['pages']; + $routes = $pages->routes(); + + // if this is an image, get the proper path + $url_bits = pathinfo($url_path); + if (isset($url_bits['extension'])) { + $target_path = $url_bits['dirname']; + } else { + $target_path = $url_path; + } + + // strip base from this path + $target_path = Utils::replaceFirstOccurrence($uri->rootUrl(), '', $target_path); + + // set to / if root + if (empty($target_path)) { + $target_path = '/'; + } + + // look to see if this page exists and has ssl enabled + if (isset($routes[$target_path])) { + $target_page = $pages->get($routes[$target_path]); + if ($target_page) { + $ssl_enabled = $target_page->ssl(); + if ($ssl_enabled !== null) { + if ($ssl_enabled) { + $url['scheme'] = 'https'; + } else { + $url['scheme'] = 'http'; + } + } + } + } + } + + // Handle route only + if ($route_only) { + $url_path = Utils::replaceFirstOccurrence(static::filterPath($base_url), '', $url_path); + } + + // transform back to string/array as needed + if (is_array($url)) { + $url['path'] = $url_path; + } else { + $url = $url_path; + } + + return $url; + } + + /** + * @param string $url + * @return array|false + */ + public static function parseUrl($url) + { + $grav = Grav::instance(); + + // Remove extra slash from streams, parse_url() doesn't like it. + if ($pos = strpos($url, ':///')) { + $url = substr_replace($url, '://', $pos, 4); + } + + $encodedUrl = preg_replace_callback( + '%[^:/@?&=#]+%usD', + static function ($matches) { + return rawurlencode($matches[0]); + }, + $url + ); + + $parts = parse_url($encodedUrl); + + if (false === $parts) { + return false; + } + + foreach ($parts as $name => $value) { + $parts[$name] = rawurldecode($value); + } + + if (!isset($parts['path'])) { + $parts['path'] = ''; + } + + [$stripped_path, $params] = static::extractParams($parts['path'], $grav['config']->get('system.param_sep')); + + if (!empty($params)) { + $parts['path'] = $stripped_path; + $parts['params'] = $params; + } + + return $parts; + } + + /** + * @param string $uri + * @param string $delimiter + * @return array + */ + public static function extractParams($uri, $delimiter) + { + $params = []; + + if (strpos($uri, $delimiter) !== false) { + preg_match_all(static::paramsRegex(), $uri, $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + $param = explode($delimiter, $match[1]); + if (count($param) === 2) { + $plain_var = filter_var(rawurldecode($param[1]), FILTER_SANITIZE_STRING); + $params[$param[0]] = $plain_var; + $uri = str_replace($match[0], '', $uri); + } + } + } + + return [$uri, $params]; + } + + /** + * Converts links from absolute '/' or relative (../..) to a Grav friendly format + * + * @param PageInterface $page the current page to use as reference + * @param string $markdown_url the URL as it was written in the markdown + * @param string $type the type of URL, image | link + * @param bool|null $relative if null, will use system default, if true will use relative links internally + * + * @return string the more friendly formatted url + */ + public static function convertUrlOld(PageInterface $page, $markdown_url, $type = 'link', $relative = null) + { + $grav = Grav::instance(); + + $language = $grav['language']; + + // Link processing should prepend language + $language_append = ''; + if ($type === 'link' && $language->enabled()) { + $language_append = $language->getLanguageURLPrefix(); + } + $pages_dir = $grav['locator']->findResource('page://'); + if ($relative === null) { + $base = $grav['base_url']; + } else { + $base = $relative ? $grav['base_url_relative'] : $grav['base_url_absolute']; + } + + $base_url = rtrim($base . $grav['pages']->base(), '/') . $language_append; + + // if absolute and starts with a base_url move on + if (pathinfo($markdown_url, PATHINFO_DIRNAME) === '.' && $page->url() === '/') { + return '/' . $markdown_url; + } + // no path to convert + if ($base_url !== '' && Utils::startsWith($markdown_url, $base_url)) { + return $markdown_url; + } + // if contains only a fragment + if (Utils::startsWith($markdown_url, '#')) { + return $markdown_url; + } + + $target = null; + // see if page is relative to this or absolute + if (Utils::startsWith($markdown_url, '/')) { + $normalized_url = Utils::normalizePath($base_url . $markdown_url); + $normalized_path = Utils::normalizePath($pages_dir . $markdown_url); + } else { + $normalized_url = $base_url . Utils::normalizePath($page->route() . '/' . $markdown_url); + $normalized_path = Utils::normalizePath($page->path() . '/' . $markdown_url); + } + + // special check to see if path checking is required. + $just_path = Utils::replaceFirstOccurrence($normalized_url, '', $normalized_path); + if ($just_path === $page->path()) { + return $normalized_url; + } + + $url_bits = parse_url($normalized_path); + $full_path = $url_bits['path']; + + if (file_exists($full_path)) { + // do nothing + } elseif (file_exists(rawurldecode($full_path))) { + $full_path = rawurldecode($full_path); + } else { + return $normalized_url; + } + + $path_info = pathinfo($full_path); + $page_path = $path_info['dirname']; + $filename = ''; + + if ($markdown_url === '..') { + $page_path = $full_path; + } else { + // save the filename if a file is part of the path + if (is_file($full_path)) { + if ($path_info['extension'] !== 'md') { + $filename = '/' . $path_info['basename']; + } + } else { + $page_path = $full_path; + } + } + + // get page instances and try to find one that fits + $instances = $grav['pages']->instances(); + if (isset($instances[$page_path])) { + /** @var PageInterface $target */ + $target = $instances[$page_path]; + $url_bits['path'] = $base_url . rtrim($target->route(), '/') . $filename; + + return static::buildUrl($url_bits); + } + + return $normalized_url; + } + + /** + * Adds the nonce to a URL for a specific action + * + * @param string $url the url + * @param string $action the action + * @param string $nonceParamName the param name to use + * + * @return string the url with the nonce + */ + public static function addNonce($url, $action, $nonceParamName = 'nonce') + { + $fake = $url && strpos($url, '/') === 0; + + if ($fake) { + $url = 'http://domain.com' . $url; + } + $uri = new static($url); + $parts = $uri->toArray(); + $nonce = Utils::getNonce($action); + $parts['params'] = ($parts['params'] ?? []) + [$nonceParamName => $nonce]; + + if ($fake) { + unset($parts['scheme'], $parts['host']); + } + + return static::buildUrl($parts); + } + + /** + * Is the passed in URL a valid URL? + * + * @param string $url + * @return bool + */ + public static function isValidUrl($url) + { + $regex = '/^(?:(https?|ftp|telnet):)?\/\/((?:[a-z0-9@:.-]|%[0-9A-F]{2}){3,})(?::(\d+))?((?:\/(?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\@]|%[0-9A-F]{2})*)*)(?:\?((?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\/?@]|%[0-9A-F]{2})*))?(?:#((?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\/?@]|%[0-9A-F]{2})*))?/'; + + return (bool)preg_match($regex, $url); + } + + /** + * Removes extra double slashes and fixes back-slashes + * + * @param string $path + * @return string + */ + public static function cleanPath($path) + { + $regex = '/(\/)\/+/'; + $path = str_replace(['\\', '/ /'], '/', $path); + $path = preg_replace($regex, '$1', $path); + + return $path; + } + + /** + * Filters the user info string. + * + * @param string|null $info The raw user or password. + * @return string The percent-encoded user or password string. + */ + public static function filterUserInfo($info) + { + return $info !== null ? UriPartsFilter::filterUserInfo($info) : ''; + } + + /** + * Filter Uri path. + * + * This method percent-encodes all reserved + * characters in the provided path string. This method + * will NOT double-encode characters that are already + * percent-encoded. + * + * @param string|null $path The raw uri path. + * @return string The RFC 3986 percent-encoded uri path. + * @link http://www.faqs.org/rfcs/rfc3986.html + */ + public static function filterPath($path) + { + return $path !== null ? UriPartsFilter::filterPath($path) : ''; + } + + /** + * Filters the query string or fragment of a URI. + * + * @param string|null $query The raw uri query string. + * @return string The percent-encoded query string. + */ + public static function filterQuery($query) + { + return $query !== null ? UriPartsFilter::filterQueryOrFragment($query) : ''; + } + + /** + * @param array $env + * @return void + */ + protected function createFromEnvironment(array $env) + { + // Build scheme. + if (isset($env['HTTP_X_FORWARDED_PROTO']) && Grav::instance()['config']->get('system.http_x_forwarded.protocol')) { + $this->scheme = $env['HTTP_X_FORWARDED_PROTO']; + } elseif (isset($env['X-FORWARDED-PROTO'])) { + $this->scheme = $env['X-FORWARDED-PROTO']; + } elseif (isset($env['HTTP_CLOUDFRONT_FORWARDED_PROTO'])) { + $this->scheme = $env['HTTP_CLOUDFRONT_FORWARDED_PROTO']; + } elseif (isset($env['REQUEST_SCHEME']) && empty($env['HTTPS'])) { + $this->scheme = $env['REQUEST_SCHEME']; + } else { + $https = $env['HTTPS'] ?? ''; + $this->scheme = (empty($https) || strtolower($https) === 'off') ? 'http' : 'https'; + } + + // Build user and password. + $this->user = $env['PHP_AUTH_USER'] ?? null; + $this->password = $env['PHP_AUTH_PW'] ?? null; + + // Build host. + if (isset($env['HTTP_X_FORWARDED_HOST']) && Grav::instance()['config']->get('system.http_x_forwarded.host')) { + $hostname = $env['HTTP_X_FORWARDED_HOST']; + } else if (isset($env['HTTP_HOST'])) { + $hostname = $env['HTTP_HOST']; + } elseif (isset($env['SERVER_NAME'])) { + $hostname = $env['SERVER_NAME']; + } else { + $hostname = 'localhost'; + } + // Remove port from HTTP_HOST generated $hostname + $hostname = Utils::substrToString($hostname, ':'); + // Validate the hostname + $this->host = $this->validateHostname($hostname) ? $hostname : 'unknown'; + + // Build port. + if (isset($env['HTTP_X_FORWARDED_PORT']) && Grav::instance()['config']->get('system.http_x_forwarded.port')) { + $this->port = (int)$env['HTTP_X_FORWARDED_PORT']; + } elseif (isset($env['X-FORWARDED-PORT'])) { + $this->port = (int)$env['X-FORWARDED-PORT']; + } elseif (isset($env['HTTP_CLOUDFRONT_FORWARDED_PROTO'])) { + // Since AWS Cloudfront does not provide a forwarded port header, + // we have to build the port using the scheme. + $this->port = $this->port(); + } elseif (isset($env['SERVER_PORT'])) { + $this->port = (int)$env['SERVER_PORT']; + } else { + $this->port = null; + } + + if ($this->port === 0 || $this->hasStandardPort()) { + $this->port = null; + } + + // Build path. + $request_uri = $env['REQUEST_URI'] ?? ''; + $this->path = rawurldecode(parse_url('http://example.com' . $request_uri, PHP_URL_PATH)); + + // Build query string. + $this->query = $env['QUERY_STRING'] ?? ''; + if ($this->query === '') { + $this->query = parse_url('http://example.com' . $request_uri, PHP_URL_QUERY); + } + + // Support ngnix routes. + if (strpos($this->query, '_url=') === 0) { + parse_str($this->query, $query); + unset($query['_url']); + $this->query = http_build_query($query); + } + + // Build fragment. + $this->fragment = null; + + // Filter userinfo, path and query string. + $this->user = $this->user !== null ? static::filterUserInfo($this->user) : null; + $this->password = $this->password !== null ? static::filterUserInfo($this->password) : null; + $this->path = empty($this->path) ? '/' : static::filterPath($this->path); + $this->query = static::filterQuery($this->query); + + $this->reset(); + } + + /** + * Does this Uri use a standard port? + * + * @return bool + */ + protected function hasStandardPort() + { + return (!$this->port || $this->port === 80 || $this->port === 443); + } + + /** + * @param string $url + */ + protected function createFromString($url) + { + // Set Uri parts. + $parts = parse_url($url); + if ($parts === false) { + throw new RuntimeException('Malformed URL: ' . $url); + } + $port = (int)($parts['port'] ?? 0); + + $this->scheme = $parts['scheme'] ?? null; + $this->user = $parts['user'] ?? null; + $this->password = $parts['pass'] ?? null; + $this->host = $parts['host'] ?? null; + $this->port = $port ?: null; + $this->path = $parts['path'] ?? ''; + $this->query = $parts['query'] ?? ''; + $this->fragment = $parts['fragment'] ?? null; + + // Validate the hostname + if ($this->host) { + $this->host = $this->validateHostname($this->host) ? $this->host : 'unknown'; + } + // Filter userinfo, path, query string and fragment. + $this->user = $this->user !== null ? static::filterUserInfo($this->user) : null; + $this->password = $this->password !== null ? static::filterUserInfo($this->password) : null; + $this->path = empty($this->path) ? '/' : static::filterPath($this->path); + $this->query = static::filterQuery($this->query); + $this->fragment = $this->fragment !== null ? static::filterQuery($this->fragment) : null; + + $this->reset(); + } + + /** + * @return void + */ + protected function reset() + { + // resets + parse_str($this->query, $this->queries); + $this->extension = null; + $this->basename = null; + $this->paths = []; + $this->params = []; + $this->env = $this->buildEnvironment(); + $this->uri = $this->path . (!empty($this->query) ? '?' . $this->query : ''); + + $this->base = $this->buildBaseUrl(); + $this->root_path = $this->buildRootPath(); + $this->root = $this->base . $this->root_path; + $this->url = $this->base . $this->uri; + } + + /** + * Get post from either $_POST or JSON response object + * By default returns all data, or can return a single item + * + * @param string|null $element + * @param string|null $filter_type + * @return array|null + */ + public function post($element = null, $filter_type = null) + { + if (!$this->post) { + $content_type = $this->getContentType(); + if ($content_type === 'application/json') { + $json = file_get_contents('php://input'); + $this->post = json_decode($json, true); + } elseif (!empty($_POST)) { + $this->post = (array)$_POST; + } + + $event = new Event(['post' => &$this->post]); + Grav::instance()->fireEvent('onHttpPostFilter', $event); + } + + if ($this->post && null !== $element) { + $item = Utils::getDotNotation($this->post, $element); + if ($filter_type) { + $item = filter_var($item, $filter_type); + } + return $item; + } + + return $this->post; + } + + /** + * Get content type from request + * + * @param bool $short + * @return null|string + */ + public function getContentType($short = true) + { + if (isset($_SERVER['CONTENT_TYPE'])) { + $content_type = $_SERVER['CONTENT_TYPE']; + if ($short) { + return Utils::substrToString($content_type, ';'); + } + return $content_type; + } + return null; + } + + /** + * Check if this is a valid Grav extension + * + * @param string|null $extension + * @return bool + */ + public function isValidExtension($extension): bool + { + $extension = (string)$extension; + + return $extension !== '' && in_array($extension, Utils::getSupportPageTypes(), true); + } + + /** + * Allow overriding of any element (be careful!) + * + * @param array $data + * @return Uri + */ + public function setUriProperties($data) + { + foreach (get_object_vars($this) as $property => $default) { + if (!array_key_exists($property, $data)) { + continue; + } + $this->{$property} = $data[$property]; // assign value to object + } + return $this; + } + + + /** + * Compatibility in case getallheaders() is not available on platform + */ + public static function getAllHeaders() + { + if (!function_exists('getallheaders')) { + $headers = []; + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } + return getallheaders(); + } + + /** + * Get the base URI with port if needed + * + * @return string + */ + private function buildBaseUrl() + { + return $this->scheme() . $this->host; + } + + /** + * Get the Grav Root Path + * + * @return string + */ + private function buildRootPath() + { + // In Windows script path uses backslash, convert it: + $scriptPath = str_replace('\\', '/', $_SERVER['PHP_SELF']); + $rootPath = str_replace(' ', '%20', rtrim(substr($scriptPath, 0, strpos($scriptPath, 'index.php')), '/')); + + return $rootPath; + } + + /** + * @return string + */ + private function buildEnvironment() + { + // check for localhost variations + if ($this->host === '127.0.0.1' || $this->host === '::1') { + return 'localhost'; + } + + return $this->host ?: 'unknown'; + } + + /** + * Process any params based in this URL, supports any valid delimiter + * + * @param string $uri + * @param string $delimiter + * @return string + */ + private function processParams(string $uri, string $delimiter = ':'): string + { + if (strpos($uri, $delimiter) !== false) { + preg_match_all(static::paramsRegex(), $uri, $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + $param = explode($delimiter, $match[1]); + if (count($param) === 2) { + $plain_var = filter_var($param[1], FILTER_SANITIZE_STRING); + $this->params[$param[0]] = $plain_var; + $uri = str_replace($match[0], '', $uri); + } + } + } + return $uri; + } +} diff --git a/system/src/Grav/Common/User/Access.php b/system/src/Grav/Common/User/Access.php new file mode 100644 index 0000000..6503c89 --- /dev/null +++ b/system/src/Grav/Common/User/Access.php @@ -0,0 +1,52 @@ + ['admin.configuration_system'], + 'admin.configuration.site' => ['admin.configuration_site', 'admin.settings'], + 'admin.configuration.media' => ['admin.configuration_media'], + 'admin.configuration.info' => ['admin.configuration_info'], + ]; + + /** + * @param string $action + * @return bool|null + */ + public function get(string $action) + { + $result = parent::get($action); + if (is_bool($result)) { + return $result; + } + + // Get access value. + if (isset($this->aliases[$action])) { + $aliases = $this->aliases[$action]; + foreach ($aliases as $alias) { + $result = parent::get($alias); + if (is_bool($result)) { + return $result; + } + } + } + + return null; + } +} diff --git a/system/src/Grav/Common/User/Authentication.php b/system/src/Grav/Common/User/Authentication.php new file mode 100644 index 0000000..6ff95e7 --- /dev/null +++ b/system/src/Grav/Common/User/Authentication.php @@ -0,0 +1,61 @@ +get('user/account'); + } + + parent::__construct($items, $blueprints); + } + + /** + * @param string $offset + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + $value = parent::offsetExists($offset); + + // Handle special case where user was logged in before 'authorized' was added to the user object. + if (false === $value && $offset === 'authorized') { + $value = $this->offsetExists('authenticated'); + } + + return $value; + } + + /** + * @param string $offset + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + $value = parent::offsetGet($offset); + + // Handle special case where user was logged in before 'authorized' was added to the user object. + if (null === $value && $offset === 'authorized') { + $value = $this->offsetGet('authenticated'); + $this->offsetSet($offset, $value); + } + + return $value; + } + + /** + * @return bool + */ + public function isValid(): bool + { + return $this->items !== null; + } + + /** + * Update object with data + * + * @param array $data + * @param array $files + * @return $this + */ + public function update(array $data, array $files = []) + { + // Note: $this->merge() would cause infinite loop as it calls this method. + parent::merge($data); + + return $this; + } + + /** + * Save user + * + * @return void + */ + public function save() + { + /** @var CompiledYamlFile|null $file */ + $file = $this->file(); + if (!$file || !$file->filename()) { + user_error(__CLASS__ . ': calling \$user = new ' . __CLASS__ . "() is deprecated since Grav 1.6, use \$grav['accounts']->load(\$username) or \$grav['accounts']->load('') instead", E_USER_DEPRECATED); + } + + if ($file) { + $username = $this->filterUsername((string)$this->get('username')); + + if (!$file->filename()) { + $locator = Grav::instance()['locator']; + $file->filename($locator->findResource('account://' . $username . YAML_EXT, true, true)); + } + + // if plain text password, hash it and remove plain text + $password = $this->get('password') ?? $this->get('password1'); + if (null !== $password && '' !== $password) { + $password2 = $this->get('password2'); + if (!\is_string($password) || ($password2 && $password !== $password2)) { + throw new \RuntimeException('Passwords did not match.'); + } + + $this->set('hashed_password', Authentication::create($password)); + } + $this->undef('password'); + $this->undef('password1'); + $this->undef('password2'); + + $data = $this->items; + if ($username === $data['username']) { + unset($data['username']); + } + unset($data['authenticated'], $data['authorized']); + + $file->save($data); + + // We need to signal Flex Users about the change. + /** @var Flex|null $flex */ + $flex = Grav::instance()['flex'] ?? null; + $users = $flex ? $flex->getDirectory('user-accounts') : null; + if (null !== $users) { + $users->clearCache(); + } + } + } + + /** + * @return MediaCollectionInterface|Media + */ + public function getMedia() + { + if (null === $this->_media) { + // Media object should only contain avatar, nothing else. + $media = new Media($this->getMediaFolder() ?? '', $this->getMediaOrder(), false); + + $path = $this->getAvatarFile(); + if ($path && is_file($path)) { + $medium = MediumFactory::fromFile($path); + if ($medium) { + $media->add(basename($path), $medium); + } + } + + $this->_media = $media; + } + + return $this->_media; + } + + /** + * @return string + */ + public function getMediaFolder() + { + return $this->blueprints()->fields()['avatar']['destination'] ?? 'user://accounts/avatars'; + } + + /** + * @return array + */ + public function getMediaOrder() + { + return []; + } + + /** + * Serialize user. + * + * @return string[] + */ + public function __sleep() + { + return [ + 'items', + 'storage' + ]; + } + + /** + * Unserialize user. + */ + public function __wakeup() + { + $this->gettersVariable = 'items'; + $this->nestedSeparator = '.'; + + if (null === $this->items) { + $this->items = []; + } + + // Always set blueprints. + if (null === $this->blueprints) { + $this->blueprints = (new Blueprints)->get('user/account'); + } + } + + /** + * Merge two configurations together. + * + * @param array $data + * @return $this + * @deprecated 1.6 Use `->update($data)` instead (same but with data validation & filtering, file upload support). + */ + public function merge(array $data) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->update($data) method instead', E_USER_DEPRECATED); + + return $this->update($data); + } + + /** + * Return media object for the User's avatar. + * + * @return Medium|null + * @deprecated 1.6 Use ->getAvatarImage() method instead. + */ + public function getAvatarMedia() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use getAvatarImage() method instead', E_USER_DEPRECATED); + + return $this->getAvatarImage(); + } + + /** + * Return the User's avatar URL + * + * @return string + * @deprecated 1.6 Use ->getAvatarUrl() method instead. + */ + public function avatarUrl() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use getAvatarUrl() method instead', E_USER_DEPRECATED); + + return $this->getAvatarUrl(); + } + + /** + * Checks user authorization to the action. + * Ensures backwards compatibility + * + * @param string $action + * @return bool + * @deprecated 1.5 Use ->authorize() method instead. + */ + public function authorise($action) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use authorize() method instead', E_USER_DEPRECATED); + + return $this->authorize($action) ?? false; + } + + /** + * Implements Countable interface. + * + * @return int + * @deprecated 1.6 Method makes no sense for user account. + */ + public function count() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED); + + return parent::count(); + } + + /** + * @param string $username + * @return string + */ + protected function filterUsername(string $username): string + { + return mb_strtolower($username); + } + + /** + * @return string|null + */ + protected function getAvatarFile(): ?string + { + $avatars = $this->get('avatar'); + if (is_array($avatars) && $avatars) { + $avatar = array_shift($avatars); + return $avatar['path'] ?? null; + } + + return null; + } +} diff --git a/system/src/Grav/Common/User/DataUser/UserCollection.php b/system/src/Grav/Common/User/DataUser/UserCollection.php new file mode 100644 index 0000000..3da7e2d --- /dev/null +++ b/system/src/Grav/Common/User/DataUser/UserCollection.php @@ -0,0 +1,162 @@ +className = $className; + } + + /** + * Load user account. + * + * Always creates user object. To check if user exists, use $this->exists(). + * + * @param string $username + * @return UserInterface + */ + public function load($username): UserInterface + { + $username = (string)$username; + + $grav = Grav::instance(); + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + // Filter username. + $username = $this->filterUsername($username); + + $filename = 'account://' . $username . YAML_EXT; + $path = $locator->findResource($filename) ?: $locator->findResource($filename, true, true); + if (!is_string($path)) { + throw new RuntimeException('Internal Error'); + } + $file = CompiledYamlFile::instance($path); + $content = (array)$file->content() + ['username' => $username, 'state' => 'enabled']; + + $userClass = $this->className; + $callable = static function () { + $blueprints = new Blueprints; + + return $blueprints->get('user/account'); + }; + + /** @var UserInterface $user */ + $user = new $userClass($content, $callable); + $user->file($file); + + return $user; + } + + /** + * Find a user by username, email, etc + * + * @param string $query the query to search for + * @param array $fields the fields to search + * @return UserInterface + */ + public function find($query, $fields = ['username', 'email']): UserInterface + { + $fields = (array)$fields; + + $grav = Grav::instance(); + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + $account_dir = $locator->findResource('account://'); + if (!is_string($account_dir)) { + return $this->load(''); + } + + $files = array_diff(scandir($account_dir) ?: [], ['.', '..']); + + // Try with username first, you never know! + if (in_array('username', $fields, true)) { + $user = $this->load($query); + unset($fields[array_search('username', $fields, true)]); + } else { + $user = $this->load(''); + } + + // If not found, try the fields + if (!$user->exists()) { + foreach ($files as $file) { + if (Utils::endsWith($file, YAML_EXT)) { + $find_user = $this->load(trim(pathinfo($file, PATHINFO_FILENAME))); + foreach ($fields as $field) { + if (isset($find_user[$field]) && $find_user[$field] === $query) { + return $find_user; + } + } + } + } + } + return $user; + } + + /** + * Remove user account. + * + * @param string $username + * @return bool True if the action was performed + */ + public function delete($username): bool + { + $file_path = Grav::instance()['locator']->findResource('account://' . $username . YAML_EXT); + + return $file_path && unlink($file_path); + } + + /** + * @return int + */ + public function count(): int + { + // check for existence of a user account + $account_dir = $file_path = Grav::instance()['locator']->findResource('account://'); + $accounts = glob($account_dir . '/*.yaml') ?: []; + + return count($accounts); + } + + /** + * @param string $username + * @return string + */ + protected function filterUsername(string $username): string + { + return mb_strtolower($username); + } +} diff --git a/system/src/Grav/Common/User/Group.php b/system/src/Grav/Common/User/Group.php new file mode 100644 index 0000000..7dd6d65 --- /dev/null +++ b/system/src/Grav/Common/User/Group.php @@ -0,0 +1,172 @@ +get('groups', []); + } + + /** + * Get the groups list + * + * @return array + * @deprecated 1.7, use $grav['user_groups'] Flex UserGroupCollection instead + */ + public static function groupNames() + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, use $grav[\'user_groups\'] Flex UserGroupCollection instead', E_USER_DEPRECATED); + + $groups = []; + + foreach (static::groups() as $groupname => $group) { + $groups[$groupname] = $group['readableName'] ?? $groupname; + } + + return $groups; + } + + /** + * Checks if a group exists + * + * @param string $groupname + * @return bool + * @deprecated 1.7, use $grav['user_groups'] Flex UserGroupCollection instead + */ + public static function groupExists($groupname) + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, use $grav[\'user_groups\'] Flex UserGroupCollection instead', E_USER_DEPRECATED); + + return isset(self::groups()[$groupname]); + } + + /** + * Get a group by name + * + * @param string $groupname + * @return object + * @deprecated 1.7, use $grav['user_groups'] Flex UserGroupCollection instead + */ + public static function load($groupname) + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, use $grav[\'user_groups\'] Flex UserGroupCollection instead', E_USER_DEPRECATED); + + $groups = self::groups(); + + $content = $groups[$groupname] ?? []; + $content += ['groupname' => $groupname]; + + $blueprints = new Blueprints(); + $blueprint = $blueprints->get('user/group'); + + return new Group($content, $blueprint); + } + + /** + * Save a group + * + * @return void + */ + public function save() + { + $grav = Grav::instance(); + + /** @var Config $config */ + $config = $grav['config']; + + $blueprints = new Blueprints(); + $blueprint = $blueprints->get('user/group'); + + $config->set("groups.{$this->get('groupname')}", []); + + $fields = $blueprint->fields(); + foreach ($fields as $field) { + if ($field['type'] === 'text') { + $value = $field['name']; + if (isset($this->items['data'][$value])) { + $config->set("groups.{$this->get('groupname')}.{$value}", $this->items['data'][$value]); + } + } + if ($field['type'] === 'array' || $field['type'] === 'permissions') { + $value = $field['name']; + $arrayValues = Utils::getDotNotation($this->items['data'], $field['name']); + + if ($arrayValues) { + foreach ($arrayValues as $arrayIndex => $arrayValue) { + $config->set("groups.{$this->get('groupname')}.{$value}.{$arrayIndex}", $arrayValue); + } + } + } + } + + $type = 'groups'; + $blueprints = $this->blueprints(); + + $filename = CompiledYamlFile::instance($grav['locator']->findResource("config://{$type}.yaml")); + + $obj = new Data($config->get($type), $blueprints); + $obj->file($filename); + $obj->save(); + } + + /** + * Remove a group + * + * @param string $groupname + * @return bool True if the action was performed + * @deprecated 1.7, use $grav['user_groups'] Flex UserGroupCollection instead + */ + public static function remove($groupname) + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, use $grav[\'user_groups\'] Flex UserGroupCollection instead', E_USER_DEPRECATED); + + $grav = Grav::instance(); + + /** @var Config $config */ + $config = $grav['config']; + + $blueprints = new Blueprints(); + $blueprint = $blueprints->get('user/group'); + + $type = 'groups'; + + $groups = $config->get($type); + unset($groups[$groupname]); + $config->set($type, $groups); + + $filename = CompiledYamlFile::instance($grav['locator']->findResource("config://{$type}.yaml")); + + $obj = new Data($groups, $blueprint); + $obj->file($filename); + $obj->save(); + + return true; + } +} diff --git a/system/src/Grav/Common/User/Interfaces/AuthorizeInterface.php b/system/src/Grav/Common/User/Interfaces/AuthorizeInterface.php new file mode 100644 index 0000000..3ad8d2c --- /dev/null +++ b/system/src/Grav/Common/User/Interfaces/AuthorizeInterface.php @@ -0,0 +1,26 @@ +exists(). + * + * @param string $username + * @return UserInterface + */ + public function load($username): UserInterface; + + /** + * Find a user by username, email, etc + * + * @param string $query the query to search for + * @param array $fields the fields to search + * @return UserInterface + */ + public function find($query, $fields = ['username', 'email']): UserInterface; + + /** + * Delete user account. + * + * @param string $username + * @return bool True if user account was found and was deleted. + */ + public function delete($username): bool; +} diff --git a/system/src/Grav/Common/User/Interfaces/UserGroupInterface.php b/system/src/Grav/Common/User/Interfaces/UserGroupInterface.php new file mode 100644 index 0000000..c345c4b --- /dev/null +++ b/system/src/Grav/Common/User/Interfaces/UserGroupInterface.php @@ -0,0 +1,18 @@ +get('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string|null $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function get($name, $default = null, $separator = null); + + /** + * Set value by using dot notation for nested arrays/objects. + * + * @example $data->set('this.is.my.nested.variable', $value); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value New value. + * @param string|null $separator Separator, defaults to '.' + * @return $this + */ + public function set($name, $value, $separator = null); + + /** + * Unset value by using dot notation for nested arrays/objects. + * + * @example $data->undef('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param string|null $separator Separator, defaults to '.' + * @return $this + */ + public function undef($name, $separator = null); + + /** + * Set default value by using dot notation for nested arrays/objects. + * + * @example $data->def('this.is.my.nested.variable', 'default'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string|null $separator Separator, defaults to '.' + * @return $this + */ + public function def($name, $default = null, $separator = null); + + /** + * Join nested values together by using blueprints. + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return $this + * @throws RuntimeException + */ + public function join($name, $value, $separator = '.'); + + /** + * Get nested structure containing default values defined in the blueprints. + * + * Fields without default value are ignored in the list. + + * @return array + */ + public function getDefaults(); + + /** + * Set default values by using blueprints. + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return $this + */ + public function joinDefaults($name, $value, $separator = '.'); + + /** + * Get value from the configuration and join it with given data. + * + * @param string $name Dot separated path to the requested value. + * @param array|object $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return array + * @throws RuntimeException + */ + public function getJoined($name, $value, $separator = '.'); + + /** + * Set default values to the configuration if variables were not set. + * + * @param array $data + * @return $this + */ + public function setDefaults(array $data); + + /** + * Update object with data + * + * @param array $data + * @param array $files + * @return $this + */ + public function update(array $data, array $files = []); + + /** + * Returns whether the data already exists in the storage. + * + * NOTE: This method does not check if the data is current. + * + * @return bool + */ + public function exists(); + + /** + * Return unmodified data as raw string. + * + * NOTE: This function only returns data which has been saved to the storage. + * + * @return string + */ + public function raw(); + + /** + * Authenticate user. + * + * If user password needs to be updated, new information will be saved. + * + * @param string $password Plaintext password. + * @return bool + */ + public function authenticate(string $password): bool; + + /** + * Return media object for the User's avatar. + * + * Note: if there's no local avatar image for the user, you should call getAvatarUrl() to get the external avatar URL. + * + * @return Medium|null + */ + public function getAvatarImage(): ?Medium; + + /** + * Return the User's avatar URL. + * + * @return string + */ + public function getAvatarUrl(): string; +} diff --git a/system/src/Grav/Common/User/Traits/UserTrait.php b/system/src/Grav/Common/User/Traits/UserTrait.php new file mode 100644 index 0000000..5a6b749 --- /dev/null +++ b/system/src/Grav/Common/User/Traits/UserTrait.php @@ -0,0 +1,187 @@ +get('hashed_password'); + + $isHashed = null !== $hash; + if (!$isHashed) { + // If there is no hashed password, fake verify with default hash. + $hash = Grav::instance()['config']->get('system.security.default_hash'); + } + + // Always execute verify() to protect us from timing attacks, but make the test to fail if hashed password wasn't set. + $result = Authentication::verify($password, $hash) && $isHashed; + + $plaintext_password = $this->get('password'); + if (null !== $plaintext_password) { + // Plain-text password is still stored, check if it matches. + if ($password !== $plaintext_password) { + return false; + } + + // Force hash update to get rid of plaintext password. + $result = 2; + } + + if ($result === 2) { + // Password needs to be updated, save the user. + $this->set('password', $password); + $this->undef('hashed_password'); + $this->save(); + } + + return (bool)$result; + } + + /** + * Checks user authorization to the action. + * + * @param string $action + * @param string|null $scope + * @return bool|null + */ + public function authorize(string $action, string $scope = null): ?bool + { + // User needs to be enabled. + if ($this->get('state', 'enabled') !== 'enabled') { + return false; + } + + // User needs to be logged in. + if (!$this->get('authenticated')) { + return false; + } + + // User needs to be authorized (2FA). + if (strpos($action, 'login') === false && !$this->get('authorized', true)) { + return false; + } + + if (null !== $scope) { + $action = $scope . '.' . $action; + } + + $config = Grav::instance()['config']; + $authorized = false; + + //Check group access level + $groups = (array)$this->get('groups'); + foreach ($groups as $group) { + $permission = $config->get("groups.{$group}.access.{$action}"); + $authorized = Utils::isPositive($permission); + if ($authorized === true) { + break; + } + } + + //Check user access level + $access = $this->get('access'); + if ($access && Utils::getDotNotation($access, $action) !== null) { + $permission = $this->get("access.{$action}"); + $authorized = Utils::isPositive($permission); + } + + return $authorized; + } + + /** + * Return media object for the User's avatar. + * + * Note: if there's no local avatar image for the user, you should call getAvatarUrl() to get the external avatar URL. + * + * @return ImageMedium|StaticImageMedium|null + */ + public function getAvatarImage(): ?Medium + { + $avatars = $this->get('avatar'); + if (is_array($avatars) && $avatars) { + $avatar = array_shift($avatars); + + $media = $this->getMedia(); + $name = $avatar['name'] ?? null; + + $image = $name ? $media[$name] : null; + if ($image instanceof ImageMedium || + $image instanceof StaticImageMedium) { + return $image; + } + } + + return null; + } + + /** + * Return the User's avatar URL + * + * @return string + */ + public function getAvatarUrl(): string + { + // Try to locate avatar image. + $avatar = $this->getAvatarImage(); + if ($avatar) { + return $avatar->url(); + } + + // Try if avatar is a sting (URL). + $avatar = $this->get('avatar'); + if (is_string($avatar)) { + return $avatar; + } + + // Try looking for provider. + $provider = $this->get('provider'); + $provider_options = $this->get($provider); + if (is_array($provider_options)) { + if (isset($provider_options['avatar_url']) && is_string($provider_options['avatar_url'])) { + return $provider_options['avatar_url']; + } + if (isset($provider_options['avatar']) && is_string($provider_options['avatar'])) { + return $provider_options['avatar']; + } + } + + $email = $this->get('email'); + + // By default fall back to gravatar image. + return $email ? 'https://www.gravatar.com/avatar/' . md5(strtolower(trim($email))) : ''; + } + + abstract public function get($name, $default = null, $separator = null); + abstract public function set($name, $value, $separator = null); + abstract public function undef($name, $separator = null); + abstract public function save(); +} diff --git a/system/src/Grav/Common/User/User.php b/system/src/Grav/Common/User/User.php new file mode 100644 index 0000000..4b2319f --- /dev/null +++ b/system/src/Grav/Common/User/User.php @@ -0,0 +1,144 @@ +exists(). + * + * @param string $username + * @return UserInterface + * @deprecated 1.6 Use $grav['accounts']->load(...) instead. + */ + public static function load($username) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use $grav[\'accounts\']->' . __FUNCTION__ . '() instead', E_USER_DEPRECATED); + + return static::getCollection()->load($username); + } + + /** + * Find a user by username, email, etc + * + * Always creates user object. To check if user exists, use $this->exists(). + * + * @param string $query the query to search for + * @param array $fields the fields to search + * @return UserInterface + * @deprecated 1.6 Use $grav['accounts']->find(...) instead. + */ + public static function find($query, $fields = ['username', 'email']) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use $grav[\'accounts\']->' . __FUNCTION__ . '() instead', E_USER_DEPRECATED); + + return static::getCollection()->find($query, $fields); + } + + /** + * Remove user account. + * + * @param string $username + * @return bool True if the action was performed + * @deprecated 1.6 Use $grav['accounts']->delete(...) instead. + */ + public static function remove($username) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use $grav[\'accounts\']->delete() instead', E_USER_DEPRECATED); + + return static::getCollection()->delete($username); + } + + /** + * @return UserCollectionInterface + */ + protected static function getCollection() + { + return Grav::instance()['accounts']; + } + } +} else { + /** + * @deprecated 1.6 Use $grav['accounts'] instead of static calls. In type hints, use UserInterface. + */ + class User extends DataUser\User + { + /** + * Load user account. + * + * Always creates user object. To check if user exists, use $this->exists(). + * + * @param string $username + * @return UserInterface + * @deprecated 1.6 Use $grav['accounts']->load(...) instead. + */ + public static function load($username) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use $grav[\'accounts\']->' . __FUNCTION__ . '() instead', E_USER_DEPRECATED); + + return static::getCollection()->load($username); + } + + /** + * Find a user by username, email, etc + * + * Always creates user object. To check if user exists, use $this->exists(). + * + * @param string $query the query to search for + * @param array $fields the fields to search + * @return UserInterface + * @deprecated 1.6 Use $grav['accounts']->find(...) instead. + */ + public static function find($query, $fields = ['username', 'email']) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use $grav[\'accounts\']->' . __FUNCTION__ . '() instead', E_USER_DEPRECATED); + + return static::getCollection()->find($query, $fields); + } + + /** + * Remove user account. + * + * @param string $username + * @return bool True if the action was performed + * @deprecated 1.6 Use $grav['accounts']->delete(...) instead. + */ + public static function remove($username) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use $grav[\'accounts\']->delete() instead', E_USER_DEPRECATED); + + return static::getCollection()->delete($username); + } + + /** + * @return UserCollectionInterface + */ + protected static function getCollection() + { + return Grav::instance()['accounts']; + } + } +} diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php new file mode 100644 index 0000000..5295a64 --- /dev/null +++ b/system/src/Grav/Common/Utils.php @@ -0,0 +1,2116 @@ +schemeExists($scheme)) { + // If scheme does not exists as a stream, assume it's external. + return str_replace(' ', '%20', $input); + } + + // Attempt to find the resource (because of parse_url() we need to put host back to path). + $resource = $locator->findResource("{$scheme}://{$host}{$path}", false); + + if ($resource === false) { + if (!$fail_gracefully) { + return false; + } + + // Return location where the file would be if it was saved. + $resource = $locator->findResource("{$scheme}://{$host}{$path}", false, true); + } + } elseif ($host || $port) { + // If URL doesn't have scheme but has host or port, it is external. + return str_replace(' ', '%20', $input); + } + + if (!empty($resource)) { + // Add query string back. + if (isset($parts['query'])) { + $resource .= '?' . $parts['query']; + } + + // Add fragment back. + if (isset($parts['fragment'])) { + $resource .= '#' . $parts['fragment']; + } + } + } else { + // Not a valid URL (can still be a stream). + $resource = $locator->findResource($input, false); + } + } else { + $root = $uri->rootUrl(); + + if (static::startsWith($input, $root)) { + $input = static::replaceFirstOccurrence($root, '', $input); + } + + $input = ltrim($input, '/'); + + $resource = $input; + } + + if (!$fail_gracefully && $resource === false) { + return false; + } + + $domain = $domain ?: $grav['config']->get('system.absolute_urls', false); + + return rtrim($uri->rootUrl($domain), '/') . '/' . ($resource ?: ''); + } + + /** + * Helper method to find the full path to a file, be it a stream, a relative path, or + * already a full path + * + * @param string $path + * @return string + */ + public static function fullPath($path) + { + $locator = Grav::instance()['locator']; + + if ($locator->isStream($path)) { + $path = $locator->findResource($path, true); + } elseif (!static::startsWith($path, GRAV_ROOT)) { + $base_url = Grav::instance()['base_url']; + $path = GRAV_ROOT . '/' . ltrim(static::replaceFirstOccurrence($base_url, '', $path), '/'); + } + + return $path; + } + + + /** + * Check if the $haystack string starts with the substring $needle + * + * @param string $haystack + * @param string|string[] $needle + * @param bool $case_sensitive + * @return bool + */ + public static function startsWith($haystack, $needle, $case_sensitive = true) + { + $status = false; + + $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos'; + + foreach ((array)$needle as $each_needle) { + $status = $each_needle === '' || $compare_func($haystack, $each_needle) === 0; + if ($status) { + break; + } + } + + return $status; + } + + /** + * Check if the $haystack string ends with the substring $needle + * + * @param string $haystack + * @param string|string[] $needle + * @param bool $case_sensitive + * @return bool + */ + public static function endsWith($haystack, $needle, $case_sensitive = true) + { + $status = false; + + $compare_func = $case_sensitive ? 'mb_strrpos' : 'mb_strripos'; + + foreach ((array)$needle as $each_needle) { + $expectedPosition = mb_strlen($haystack) - mb_strlen($each_needle); + $status = $each_needle === '' || $compare_func($haystack, $each_needle, 0) === $expectedPosition; + if ($status) { + break; + } + } + + return $status; + } + + /** + * Check if the $haystack string contains the substring $needle + * + * @param string $haystack + * @param string|string[] $needle + * @param bool $case_sensitive + * @return bool + */ + public static function contains($haystack, $needle, $case_sensitive = true) + { + $status = false; + + $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos'; + + foreach ((array)$needle as $each_needle) { + $status = $each_needle === '' || $compare_func($haystack, $each_needle) !== false; + if ($status) { + break; + } + } + + return $status; + } + + /** + * Function that can match wildcards + * + * match_wildcard('foo*', $test), // TRUE + * match_wildcard('bar*', $test), // FALSE + * match_wildcard('*bar*', $test), // TRUE + * match_wildcard('**blob**', $test), // TRUE + * match_wildcard('*a?d*', $test), // TRUE + * match_wildcard('*etc**', $test) // TRUE + * + * @param string $wildcard_pattern + * @param string $haystack + * @return false|int + */ + public static function matchWildcard($wildcard_pattern, $haystack) + { + $regex = str_replace( + array("\*", "\?"), // wildcard chars + array('.*', '.'), // regexp chars + preg_quote($wildcard_pattern, '/') + ); + + return preg_match('/^' . $regex . '$/is', $haystack); + } + + /** + * Render simple template filling up the variables in it. If value is not defined, leave it as it was. + * + * @param string $template Template string + * @param array $variables Variables with values + * @param array $brackets Optional array of opening and closing brackets or symbols + * @return string Final string filled with values + */ + public static function simpleTemplate(string $template, array $variables, array $brackets = ['{', '}']): string + { + $opening = $brackets[0] ?? '{'; + $closing = $brackets[1] ?? '}'; + $expression = '/' . preg_quote($opening, '/') . '(.*?)' . preg_quote($closing, '/') . '/'; + $callback = static function ($match) use ($variables) { + return $variables[$match[1]] ?? $match[0]; + }; + + return preg_replace_callback($expression, $callback, $template); + } + + /** + * Returns the substring of a string up to a specified needle. if not found, return the whole haystack + * + * @param string $haystack + * @param string $needle + * @param bool $case_sensitive + * + * @return string + */ + public static function substrToString($haystack, $needle, $case_sensitive = true) + { + $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos'; + + if (static::contains($haystack, $needle, $case_sensitive)) { + return mb_substr($haystack, 0, $compare_func($haystack, $needle, $case_sensitive)); + } + + return $haystack; + } + + /** + * Utility method to replace only the first occurrence in a string + * + * @param string $search + * @param string $replace + * @param string $subject + * + * @return string + */ + public static function replaceFirstOccurrence($search, $replace, $subject) + { + if (!$search) { + return $subject; + } + + $pos = mb_strpos($subject, $search); + if ($pos !== false) { + $subject = static::mb_substr_replace($subject, $replace, $pos, mb_strlen($search)); + } + + + return $subject; + } + + /** + * Utility method to replace only the last occurrence in a string + * + * @param string $search + * @param string $replace + * @param string $subject + * @return string + */ + public static function replaceLastOccurrence($search, $replace, $subject) + { + $pos = strrpos($subject, $search); + + if ($pos !== false) { + $subject = static::mb_substr_replace($subject, $replace, $pos, mb_strlen($search)); + } + + return $subject; + } + + /** + * Multibyte compatible substr_replace + * + * @param string $original + * @param string $replacement + * @param int $position + * @param int $length + * @return string + */ + public static function mb_substr_replace($original, $replacement, $position, $length) + { + $startString = mb_substr($original, 0, $position, 'UTF-8'); + $endString = mb_substr($original, $position + $length, mb_strlen($original), 'UTF-8'); + + return $startString . $replacement . $endString; + } + + /** + * Merge two objects into one. + * + * @param object $obj1 + * @param object $obj2 + * + * @return object + */ + public static function mergeObjects($obj1, $obj2) + { + return (object)array_merge((array)$obj1, (array)$obj2); + } + + /** + * @param array $array + * @return bool + */ + public static function isAssoc(array $array) + { + return (array_values($array) !== $array); + } + + /** + * Lowercase an entire array. Useful when combined with `in_array()` + * + * @param array $a + * @return array|false + */ + public static function arrayLower(array $a) + { + return array_map('mb_strtolower', $a); + } + + /** + * Simple function to remove item/s in an array by value + * + * @param array $search + * @param string|array $value + * @return array + */ + public static function arrayRemoveValue(array $search, $value) + { + foreach ((array)$value as $val) { + $key = array_search($val, $search); + if ($key !== false) { + unset($search[$key]); + } + } + return $search; + } + + /** + * Recursive Merge with uniqueness + * + * @param array $array1 + * @param array $array2 + * @return array + */ + public static function arrayMergeRecursiveUnique($array1, $array2) + { + if (empty($array1)) { + // Optimize the base case + return $array2; + } + + foreach ($array2 as $key => $value) { + if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) { + $value = static::arrayMergeRecursiveUnique($array1[$key], $value); + } + $array1[$key] = $value; + } + + return $array1; + } + + /** + * Returns an array with the differences between $array1 and $array2 + * + * @param array $array1 + * @param array $array2 + * @return array + */ + public static function arrayDiffMultidimensional($array1, $array2) + { + $result = array(); + foreach ($array1 as $key => $value) { + if (!is_array($array2) || !array_key_exists($key, $array2)) { + $result[$key] = $value; + continue; + } + if (is_array($value)) { + $recursiveArrayDiff = static::ArrayDiffMultidimensional($value, $array2[$key]); + if (count($recursiveArrayDiff)) { + $result[$key] = $recursiveArrayDiff; + } + continue; + } + if ($value != $array2[$key]) { + $result[$key] = $value; + } + } + + return $result; + } + + /** + * Array combine but supports different array lengths + * + * @param array $arr1 + * @param array $arr2 + * @return array|false + */ + public static function arrayCombine($arr1, $arr2) + { + $count = min(count($arr1), count($arr2)); + + return array_combine(array_slice($arr1, 0, $count), array_slice($arr2, 0, $count)); + } + + /** + * Array is associative or not + * + * @param array $arr + * @return bool + */ + public static function arrayIsAssociative($arr) + { + if ([] === $arr) { + return false; + } + + return array_keys($arr) !== range(0, count($arr) - 1); + } + + /** + * Return the Grav date formats allowed + * + * @return array + */ + public static function dateFormats() + { + $now = new DateTime(); + + $date_formats = [ + 'd-m-Y H:i' => 'd-m-Y H:i (e.g. ' . $now->format('d-m-Y H:i') . ')', + 'Y-m-d H:i' => 'Y-m-d H:i (e.g. ' . $now->format('Y-m-d H:i') . ')', + 'm/d/Y h:i a' => 'm/d/Y h:i a (e.g. ' . $now->format('m/d/Y h:i a') . ')', + 'H:i d-m-Y' => 'H:i d-m-Y (e.g. ' . $now->format('H:i d-m-Y') . ')', + 'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. ' . $now->format('h:i a m/d/Y') . ')', + ]; + $default_format = Grav::instance()['config']->get('system.pages.dateformat.default'); + if ($default_format) { + $date_formats = array_merge([$default_format => $default_format . ' (e.g. ' . $now->format($default_format) . ')'], $date_formats); + } + + return $date_formats; + } + + /** + * Get current date/time + * + * @param string|null $default_format + * @return string + * @throws Exception + */ + public static function dateNow($default_format = null) + { + $now = new DateTime(); + + if (null === $default_format) { + $default_format = Grav::instance()['config']->get('system.pages.dateformat.default'); + } + + return $now->format($default_format); + } + + /** + * Truncate text by number of characters but can cut off words. + * + * @param string $string + * @param int $limit Max number of characters. + * @param bool $up_to_break truncate up to breakpoint after char count + * @param string $break Break point. + * @param string $pad Appended padding to the end of the string. + * @return string + */ + public static function truncate($string, $limit = 150, $up_to_break = false, $break = ' ', $pad = '…') + { + // return with no change if string is shorter than $limit + if (mb_strlen($string) <= $limit) { + return $string; + } + + // is $break present between $limit and the end of the string? + if ($up_to_break && false !== ($breakpoint = mb_strpos($string, $break, $limit))) { + if ($breakpoint < mb_strlen($string) - 1) { + $string = mb_substr($string, 0, $breakpoint) . $pad; + } + } else { + $string = mb_substr($string, 0, $limit) . $pad; + } + + return $string; + } + + /** + * Truncate text by number of characters in a "word-safe" manor. + * + * @param string $string + * @param int $limit + * @return string + */ + public static function safeTruncate($string, $limit = 150) + { + return static::truncate($string, $limit, true); + } + + + /** + * Truncate HTML by number of characters. not "word-safe"! + * + * @param string $text + * @param int $length in characters + * @param string $ellipsis + * @return string + */ + public static function truncateHtml($text, $length = 100, $ellipsis = '...') + { + return Truncator::truncateLetters($text, $length, $ellipsis); + } + + /** + * Truncate HTML by number of characters in a "word-safe" manor. + * + * @param string $text + * @param int $length in words + * @param string $ellipsis + * @return string + */ + public static function safeTruncateHtml($text, $length = 25, $ellipsis = '...') + { + return Truncator::truncateWords($text, $length, $ellipsis); + } + + /** + * Generate a random string of a given length + * + * @param int $length + * @return string + */ + public static function generateRandomString($length = 5) + { + return substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length); + } + + /** + * Generates a random string with configurable length, prefix and suffix. + * Unlike the built-in `uniqid()`, this string is non-conflicting and safe + * + * @param int $length + * @param array $options + * @return string + * @throws Exception + */ + public static function uniqueId(int $length = 13, array $options = []): string + { + $options = array_merge(['prefix' => '', 'suffix' => ''], $options); + $bytes = random_bytes(ceil($length / 2)); + + return $options['prefix'] . substr(bin2hex($bytes), 0, $length) . $options['suffix']; + } + + /** + * Provides the ability to download a file to the browser + * + * @param string $file the full path to the file to be downloaded + * @param bool $force_download as opposed to letting browser choose if to download or render + * @param int $sec Throttling, try 0.1 for some speed throttling of downloads + * @param int $bytes Size of chunks to send in bytes. Default is 1024 + * @throws Exception + */ + public static function download($file, $force_download = true, $sec = 0, $bytes = 1024) + { + if (file_exists($file)) { + // fire download event + Grav::instance()->fireEvent('onBeforeDownload', new Event(['file' => $file])); + + $file_parts = pathinfo($file); + $mimetype = static::getMimeByExtension($file_parts['extension']); + $size = filesize($file); // File size + + // clean all buffers + while (ob_get_level()) { + ob_end_clean(); + } + + // required for IE, otherwise Content-Disposition may be ignored + if (ini_get('zlib.output_compression')) { + ini_set('zlib.output_compression', 'Off'); + } + + header('Content-Type: ' . $mimetype); + header('Accept-Ranges: bytes'); + + if ($force_download) { + // output the regular HTTP headers + header('Content-Disposition: attachment; filename="' . $file_parts['basename'] . '"'); + } + + // multipart-download and download resuming support + if (isset($_SERVER['HTTP_RANGE'])) { + [$a, $range] = explode('=', $_SERVER['HTTP_RANGE'], 2); + [$range] = explode(',', $range, 2); + [$range, $range_end] = explode('-', $range); + $range = (int)$range; + if (!$range_end) { + $range_end = $size - 1; + } else { + $range_end = (int)$range_end; + } + $new_length = $range_end - $range + 1; + header('HTTP/1.1 206 Partial Content'); + header("Content-Length: {$new_length}"); + header("Content-Range: bytes {$range}-{$range_end}/{$size}"); + } else { + $range = 0; + $new_length = $size; + header('Content-Length: ' . $size); + + if (Grav::instance()['config']->get('system.cache.enabled')) { + $expires = Grav::instance()['config']->get('system.pages.expires'); + if ($expires > 0) { + $expires_date = gmdate('D, d M Y H:i:s T', time() + $expires); + header('Cache-Control: max-age=' . $expires); + header('Expires: ' . $expires_date); + header('Pragma: cache'); + } + header('Last-Modified: ' . gmdate('D, d M Y H:i:s T', filemtime($file))); + + // Return 304 Not Modified if the file is already cached in the browser + if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && + strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= filemtime($file)) { + header('HTTP/1.1 304 Not Modified'); + exit(); + } + } + } + + /* output the file itself */ + $chunksize = $bytes * 8; //you may want to change this + $bytes_send = 0; + + $fp = @fopen($file, 'rb'); + if ($fp) { + if ($range) { + fseek($fp, $range); + } + while (!feof($fp) && (!connection_aborted()) && ($bytes_send < $new_length)) { + $buffer = fread($fp, $chunksize); + echo($buffer); //echo($buffer); // is also possible + flush(); + usleep($sec * 1000000); + $bytes_send += strlen($buffer); + } + fclose($fp); + } else { + throw new RuntimeException('Error - can not open file.'); + } + + exit; + } + } + + /** + * Returns the output render format, usually the extension provided in the URL. (e.g. `html`, `json`, `xml`, etc). + * + * @return string + */ + public static function getPageFormat(): string + { + /** @var Uri $uri */ + $uri = Grav::instance()['uri']; + + // Set from uri extension + $uri_extension = $uri->extension(); + if (is_string($uri_extension) && $uri->isValidExtension($uri_extension)) { + return ($uri_extension); + } + + // Use content negotiation via the `accept:` header + $http_accept = $_SERVER['HTTP_ACCEPT'] ?? null; + if (is_string($http_accept)) { + $negotiator = new Negotiator(); + + $supported_types = static::getSupportPageTypes(['html', 'json']); + $priorities = static::getMimeTypes($supported_types); + + $media_type = $negotiator->getBest($http_accept, $priorities); + $mimetype = $media_type instanceof Accept ? $media_type->getValue() : ''; + + return static::getExtensionByMime($mimetype); + } + + return 'html'; + } + + /** + * Return the mimetype based on filename extension + * + * @param string $extension Extension of file (eg "txt") + * @param string $default + * @return string + */ + public static function getMimeByExtension($extension, $default = 'application/octet-stream') + { + $extension = strtolower($extension); + + // look for some standard types + switch ($extension) { + case null: + return $default; + case 'json': + return 'application/json'; + case 'html': + return 'text/html'; + case 'atom': + return 'application/atom+xml'; + case 'rss': + return 'application/rss+xml'; + case 'xml': + return 'application/xml'; + } + + $media_types = Grav::instance()['config']->get('media.types'); + + return $media_types[$extension]['mime'] ?? $default; + } + + /** + * Get all the mimetypes for an array of extensions + * + * @param array $extensions + * @return array + */ + public static function getMimeTypes(array $extensions) + { + $mimetypes = []; + foreach ($extensions as $extension) { + $mimetype = static::getMimeByExtension($extension, false); + if ($mimetype && !in_array($mimetype, $mimetypes)) { + $mimetypes[] = $mimetype; + } + } + return $mimetypes; + } + + + /** + * Return the mimetype based on filename extension + * + * @param string $mime mime type (eg "text/html") + * @param string $default default value + * @return string + */ + public static function getExtensionByMime($mime, $default = 'html') + { + $mime = strtolower($mime); + + // look for some standard mime types + switch ($mime) { + case '*/*': + case 'text/*': + case 'text/html': + return 'html'; + case 'application/json': + return 'json'; + case 'application/atom+xml': + return 'atom'; + case 'application/rss+xml': + return 'rss'; + case 'application/xml': + return 'xml'; + } + + $media_types = (array)Grav::instance()['config']->get('media.types'); + + foreach ($media_types as $extension => $type) { + if ($extension === 'defaults') { + continue; + } + if (isset($type['mime']) && $type['mime'] === $mime) { + return $extension; + } + } + + return $default; + } + + /** + * Get all the extensions for an array of mimetypes + * + * @param array $mimetypes + * @return array + */ + public static function getExtensions(array $mimetypes) + { + $extensions = []; + foreach ($mimetypes as $mimetype) { + $extension = static::getExtensionByMime($mimetype, false); + if ($extension && !in_array($extension, $extensions, true)) { + $extensions[] = $extension; + } + } + + return $extensions; + } + + /** + * Return the mimetype based on filename + * + * @param string $filename Filename or path to file + * @param string $default default value + * @return string + */ + public static function getMimeByFilename($filename, $default = 'application/octet-stream') + { + return static::getMimeByExtension(pathinfo($filename, PATHINFO_EXTENSION), $default); + } + + /** + * Return the mimetype based on existing local file + * + * @param string $filename Path to the file + * @param string $default + * @return string|bool + */ + public static function getMimeByLocalFile($filename, $default = 'application/octet-stream') + { + $type = false; + + // For local files we can detect type by the file content. + if (!stream_is_local($filename) || !file_exists($filename)) { + return false; + } + + // Prefer using finfo if it exists. + if (extension_loaded('fileinfo')) { + $finfo = finfo_open(FILEINFO_SYMLINK | FILEINFO_MIME_TYPE); + $type = finfo_file($finfo, $filename); + finfo_close($finfo); + } else { + // Fall back to use getimagesize() if it is available (not recommended, but better than nothing) + $info = @getimagesize($filename); + if ($info) { + $type = $info['mime']; + } + } + + return $type ?: static::getMimeByFilename($filename, $default); + } + + + /** + * Returns true if filename is considered safe. + * + * @param string $filename + * @return bool + */ + public static function checkFilename($filename) + { + $dangerous_extensions = Grav::instance()['config']->get('security.uploads_dangerous_extensions', []); + $extension = pathinfo($filename, PATHINFO_EXTENSION); + + return !( + // Empty filenames are not allowed. + !$filename + // Filename should not contain horizontal/vertical tabs, newlines, nils or back/forward slashes. + || strtr($filename, "\t\v\n\r\0\\/", '_______') !== $filename + // Filename should not start or end with dot or space. + || trim($filename, '. ') !== $filename + // File extension should not be part of configured dangerous extensions + || in_array($extension, $dangerous_extensions) + ); + } + + /** + * Normalize path by processing relative `.` and `..` syntax and merging path + * + * @param string $path + * @return string + */ + public static function normalizePath($path) + { + // Resolve any streams + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + if ($locator->isStream($path)) { + $path = $locator->findResource($path); + } + + // Set root properly for any URLs + $root = ''; + preg_match(self::ROOTURL_REGEX, $path, $matches); + if ($matches) { + $root = $matches[1]; + $path = $matches[2]; + } + + // Strip off leading / to ensure explode is accurate + if (static::startsWith($path, '/')) { + $root .= '/'; + $path = ltrim($path, '/'); + } + + // If there are any relative paths (..) handle those + if (static::contains($path, '..')) { + $segments = explode('/', trim($path, '/')); + $ret = []; + foreach ($segments as $segment) { + if (($segment === '.') || $segment === '') { + continue; + } + if ($segment === '..') { + array_pop($ret); + } else { + $ret[] = $segment; + } + } + $path = implode('/', $ret); + } + + // Stick everything back together + $normalized = $root . $path; + + return $normalized; + } + + /** + * Check whether a function exists. + * + * Disabled functions count as non-existing functions, just like in PHP 8+. + * + * @param string $function the name of the function to check + * @return bool + */ + public static function functionExists($function): bool + { + if (!function_exists($function)) { + return false; + } + + // In PHP 7 we need to also exclude disabled methods. + return !static::isFunctionDisabled($function); + } + + /** + * Check whether a function is disabled in the PHP settings + * + * @param string $function the name of the function to check + * @return bool + */ + public static function isFunctionDisabled($function): bool + { + static $list; + + if (null === $list) { + $str = trim(ini_get('disable_functions') . ',' . ini_get('suhosin.executor.func.blacklist'), ','); + $list = $str ? array_flip(preg_split('/\s*,\s*/', $str)) : []; + } + + return array_key_exists($function, $list); + } + + /** + * Get the formatted timezones list + * + * @return array + */ + public static function timezones() + { + $timezones = DateTimeZone::listIdentifiers(DateTimeZone::ALL); + $offsets = []; + $testDate = new DateTime(); + + foreach ($timezones as $zone) { + $tz = new DateTimeZone($zone); + $offsets[$zone] = $tz->getOffset($testDate); + } + + asort($offsets); + + $timezone_list = []; + foreach ($offsets as $timezone => $offset) { + $offset_prefix = $offset < 0 ? '-' : '+'; + $offset_formatted = gmdate('H:i', abs($offset)); + + $pretty_offset = "UTC${offset_prefix}${offset_formatted}"; + + $timezone_list[$timezone] = "(${pretty_offset}) " . str_replace('_', ' ', $timezone); + } + + return $timezone_list; + } + + /** + * Recursively filter an array, filtering values by processing them through the $fn function argument + * + * @param array $source the Array to filter + * @param callable $fn the function to pass through each array item + * @return array + */ + public static function arrayFilterRecursive(array $source, $fn) + { + $result = []; + foreach ($source as $key => $value) { + if (is_array($value)) { + $result[$key] = static::arrayFilterRecursive($value, $fn); + continue; + } + if ($fn($key, $value)) { + $result[$key] = $value; // KEEP + continue; + } + } + + return $result; + } + + /** + * Flatten a multi-dimensional associative array into query params. + * + * @param array $array + * @param string $prepend + * @return array + */ + public static function arrayToQueryParams($array, $prepend = '') + { + $results = []; + foreach ($array as $key => $value) { + $name = $prepend ? $prepend . '[' . $key . ']' : $key; + + if (is_array($value)) { + $results = array_merge($results, static::arrayToQueryParams($value, $name)); + } else { + $results[$name] = $value; + } + } + + return $results; + } + + /** + * Flatten an array + * + * @param array $array + * @return array + */ + public static function arrayFlatten($array) + { + $flatten = []; + foreach ($array as $key => $inner) { + if (is_array($inner)) { + foreach ($inner as $inner_key => $value) { + $flatten[$inner_key] = $value; + } + } else { + $flatten[$key] = $inner; + } + } + + return $flatten; + } + + /** + * Flatten a multi-dimensional associative array into dot notation + * + * @param array $array + * @param string $prepend + * @return array + */ + public static function arrayFlattenDotNotation($array, $prepend = '') + { + $results = array(); + foreach ($array as $key => $value) { + if (is_array($value)) { + $results = array_merge($results, static::arrayFlattenDotNotation($value, $prepend . $key . '.')); + } else { + $results[$prepend . $key] = $value; + } + } + + return $results; + } + + /** + * Opposite of flatten, convert flat dot notation array to multi dimensional array. + * + * If any of the parent has a scalar value, all children get ignored: + * + * admin.pages=true + * admin.pages.read=true + * + * becomes + * + * admin: + * pages: true + * + * @param array $array + * @param string $separator + * @return array + */ + public static function arrayUnflattenDotNotation($array, $separator = '.') + { + $newArray = []; + foreach ($array as $key => $value) { + $dots = explode($separator, $key); + if (count($dots) > 1) { + $last = &$newArray[$dots[0]]; + foreach ($dots as $k => $dot) { + if ($k === 0) { + continue; + } + + // Cannot use a scalar value as an array + if (null !== $last && !is_array($last)) { + continue 2; + } + + $last = &$last[$dot]; + } + + // Cannot use a scalar value as an array + if (null !== $last && !is_array($last)) { + continue; + } + + $last = $value; + } else { + $newArray[$key] = $value; + } + } + + return $newArray; + } + + /** + * Checks if the passed path contains the language code prefix + * + * @param string $string The path + * + * @return bool|string Either false or the language + * + */ + public static function pathPrefixedByLangCode($string) + { + $languages_enabled = Grav::instance()['config']->get('system.languages.supported', []); + $parts = explode('/', trim($string, '/')); + + if (count($parts) > 0 && in_array($parts[0], $languages_enabled)) { + return $parts[0]; + } + return false; + } + + /** + * Get the timestamp of a date + * + * @param string $date a String expressed in the system.pages.dateformat.default format, with fallback to a + * strtotime argument + * @param string|null $format a date format to use if possible + * @return int the timestamp + */ + public static function date2timestamp($date, $format = null) + { + $config = Grav::instance()['config']; + $dateformat = $format ?: $config->get('system.pages.dateformat.default'); + + // try to use DateTime and default format + if ($dateformat) { + $datetime = DateTime::createFromFormat($dateformat, $date); + } else { + $datetime = new DateTime($date); + } + + // fallback to strtotime() if DateTime approach failed + if ($datetime !== false) { + return $datetime->getTimestamp(); + } + + return strtotime($date); + } + + /** + * @param array $array + * @param string $path + * @param null $default + * @return mixed + * + * @deprecated 1.5 Use ->getDotNotation() method instead. + */ + public static function resolve(array $array, $path, $default = null) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->getDotNotation() method instead', E_USER_DEPRECATED); + + return static::getDotNotation($array, $path, $default); + } + + /** + * Checks if a value is positive (true) + * + * @param string $value + * @return bool + */ + public static function isPositive($value) + { + return in_array($value, [true, 1, '1', 'yes', 'on', 'true'], true); + } + + /** + * Checks if a value is negative (false) + * + * @param string $value + * @return bool + */ + public static function isNegative($value) + { + return in_array($value, [false, 0, '0', 'no', 'off', 'false'], true); + } + + /** + * Generates a nonce string to be hashed. Called by self::getNonce() + * We removed the IP portion in this version because it causes too many inconsistencies + * with reverse proxy setups. + * + * @param string $action + * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours) + * @return string the nonce string + */ + private static function generateNonceString($action, $previousTick = false) + { + $grav = Grav::instance(); + + $username = isset($grav['user']) ? $grav['user']->username : ''; + $token = session_id(); + $i = self::nonceTick(); + + if ($previousTick) { + $i--; + } + + return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . $grav['config']->get('security.salt')); + } + + /** + * Get the time-dependent variable for nonce creation. + * + * Now a tick lasts a day. Once the day is passed, the nonce is not valid any more. Find a better way + * to ensure nonces issued near the end of the day do not expire in that small amount of time + * + * @return int the time part of the nonce. Changes once every 24 hours + */ + private static function nonceTick() + { + $secondsInHalfADay = 60 * 60 * 12; + + return (int)ceil(time() / $secondsInHalfADay); + } + + /** + * Creates a hashed nonce tied to the passed action. Tied to the current user and time. The nonce for a given + * action is the same for 12 hours. + * + * @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage) + * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours) + * @return string the nonce + */ + public static function getNonce($action, $previousTick = false) + { + // Don't regenerate this again if not needed + if (isset(static::$nonces[$action][$previousTick])) { + return static::$nonces[$action][$previousTick]; + } + $nonce = md5(self::generateNonceString($action, $previousTick)); + static::$nonces[$action][$previousTick] = $nonce; + + return static::$nonces[$action][$previousTick]; + } + + /** + * Verify the passed nonce for the give action + * + * @param string|string[] $nonce the nonce to verify + * @param string $action the action to verify the nonce to + * @return boolean verified or not + */ + public static function verifyNonce($nonce, $action) + { + //Safety check for multiple nonces + if (is_array($nonce)) { + $nonce = array_shift($nonce); + } + + //Nonce generated 0-12 hours ago + if ($nonce === self::getNonce($action)) { + return true; + } + + //Nonce generated 12-24 hours ago + return $nonce === self::getNonce($action, true); + } + + /** + * Simple helper method to get whether or not the admin plugin is active + * + * @return bool + */ + public static function isAdminPlugin() + { + return isset(Grav::instance()['admin']); + } + + /** + * Get a portion of an array (passed by reference) with dot-notation key + * + * @param array $array + * @param string|int|null $key + * @param null $default + * @return mixed + */ + public static function getDotNotation($array, $key, $default = null) + { + if (null === $key) { + return $array; + } + + if (isset($array[$key])) { + return $array[$key]; + } + + foreach (explode('.', $key) as $segment) { + if (!is_array($array) || !array_key_exists($segment, $array)) { + return $default; + } + + $array = $array[$segment]; + } + + return $array; + } + + /** + * Set portion of array (passed by reference) for a dot-notation key + * and set the value + * + * @param array $array + * @param string|int|null $key + * @param mixed $value + * @param bool $merge + * + * @return mixed + */ + public static function setDotNotation(&$array, $key, $value, $merge = false) + { + if (null === $key) { + return $array = $value; + } + + $keys = explode('.', $key); + + while (count($keys) > 1) { + $key = array_shift($keys); + + if (!isset($array[$key]) || !is_array($array[$key])) { + $array[$key] = array(); + } + + $array =& $array[$key]; + } + + $key = array_shift($keys); + + if (!$merge || !isset($array[$key])) { + $array[$key] = $value; + } else { + $array[$key] = array_merge($array[$key], $value); + } + + return $array; + } + + /** + * Utility method to determine if the current OS is Windows + * + * @return bool + */ + public static function isWindows() + { + return strncasecmp(PHP_OS, 'WIN', 3) === 0; + } + + /** + * Utility to determine if the server running PHP is Apache + * + * @return bool + */ + public static function isApache() + { + return isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false; + } + + /** + * Sort a multidimensional array by another array of ordered keys + * + * @param array $array + * @param array $orderArray + * @return array + */ + public static function sortArrayByArray(array $array, array $orderArray) + { + $ordered = []; + foreach ($orderArray as $key) { + if (array_key_exists($key, $array)) { + $ordered[$key] = $array[$key]; + unset($array[$key]); + } + } + return $ordered + $array; + } + + /** + * Sort an array by a key value in the array + * + * @param mixed $array + * @param string|int $array_key + * @param int $direction + * @param int $sort_flags + * @return array + */ + public static function sortArrayByKey($array, $array_key, $direction = SORT_DESC, $sort_flags = SORT_REGULAR) + { + $output = []; + + if (!is_array($array) || !$array) { + return $output; + } + + foreach ($array as $key => $row) { + $output[$key] = $row[$array_key]; + } + + array_multisort($output, $direction, $sort_flags, $array); + + return $array; + } + + /** + * Get relative page path based on a token. + * + * @param string $path + * @param PageInterface|null $page + * @return string + * @throws RuntimeException + */ + public static function getPagePathFromToken($path, PageInterface $page = null) + { + return static::getPathFromToken($path, $page); + } + + /** + * Get relative path based on a token. + * + * Path supports following syntaxes: + * + * 'self@', 'self@/path' + * 'page@:/route', 'page@:/route/filename.ext' + * 'theme@:', 'theme@:/path' + * + * @param string $path + * @param FlexObjectInterface|PageInterface|null $object + * @return string + * @throws RuntimeException + */ + public static function getPathFromToken($path, $object = null) + { + $matches = static::resolveTokenPath($path); + if (null === $matches) { + return $path; + } + + $grav = Grav::instance(); + + switch ($matches[0]) { + case 'self': + if (!$object instanceof MediaInterface) { + throw new RuntimeException(sprintf('Page not available for self@ reference: %s', $path)); + } + + if ($matches[2] === '') { + if ($object->exists()) { + $route = '/' . $matches[1]; + + if ($object instanceof PageInterface) { + return trim($object->relativePagePath() . $route, '/'); + } + + $folder = $object->getMediaFolder(); + if ($folder) { + return trim($folder . $route, '/'); + } + } else { + return ''; + } + } + + break; + case 'page': + if ($matches[1] === '') { + $route = '/' . $matches[2]; + + // Exclude filename from the page lookup. + if (pathinfo($route, PATHINFO_EXTENSION)) { + $basename = '/' . basename($route); + $route = \dirname($route); + } else { + $basename = ''; + } + + $key = trim($route === '/' ? $grav['config']->get('system.home.alias') : $route, '/'); + if ($object instanceof PageObject) { + $object = $object->getFlexDirectory()->getObject($key); + } elseif (static::isAdminPlugin()) { + /** @var Flex|null $flex */ + $flex = $grav['flex'] ?? null; + $object = $flex ? $flex->getObject($key, 'pages') : null; + } else { + /** @var Pages $pages */ + $pages = $grav['pages']; + $object = $pages->find($route); + } + + if ($object instanceof PageInterface) { + return trim($object->relativePagePath() . $basename, '/'); + } + } + + break; + case 'theme': + if ($matches[1] === '') { + $route = '/' . $matches[2]; + $theme = $grav['locator']->findResource('theme://', false); + if (false !== $theme) { + return trim($theme . $route, '/'); + } + } + + break; + } + + throw new RuntimeException(sprintf('Token path not found: %s', $path)); + } + + /** + * Returns [token, route, path] from '@token/route:/path'. Route and path are optional. If pattern does not match, return null. + * + * @param string $path + * @return string[]|null + */ + protected static function resolveTokenPath(string $path): ?array + { + if (strpos($path, '@') !== false) { + $regex = '/^(@\w+|\w+@|@\w+@)([^:]*)(.*)$/u'; + if (preg_match($regex, $path, $matches)) { + return [ + trim($matches[1], '@'), + trim($matches[2], '/'), + trim($matches[3], ':/') + ]; + } + } + + return null; + } + + /** + * @return int + */ + public static function getUploadLimit() + { + static $max_size = -1; + + if ($max_size < 0) { + $post_max_size = static::parseSize(ini_get('post_max_size')); + if ($post_max_size > 0) { + $max_size = $post_max_size; + } else { + $max_size = 0; + } + + $upload_max = static::parseSize(ini_get('upload_max_filesize')); + if ($upload_max > 0 && $upload_max < $max_size) { + $max_size = $upload_max; + } + } + + return $max_size; + } + + /** + * Convert bytes to the unit specified by the $to parameter. + * + * @param int $bytes The filesize in Bytes. + * @param string $to The unit type to convert to. Accepts K, M, or G for Kilobytes, Megabytes, or Gigabytes, respectively. + * @param int $decimal_places The number of decimal places to return. + * @return int Returns only the number of units, not the type letter. Returns 0 if the $to unit type is out of scope. + * + */ + public static function convertSize($bytes, $to, $decimal_places = 1) + { + $formulas = array( + 'K' => number_format($bytes / 1024, $decimal_places), + 'M' => number_format($bytes / 1048576, $decimal_places), + 'G' => number_format($bytes / 1073741824, $decimal_places) + ); + return $formulas[$to] ?? 0; + } + + /** + * Return a pretty size based on bytes + * + * @param int $bytes + * @param int $precision + * @return string + */ + public static function prettySize($bytes, $precision = 2) + { + $units = array('B', 'KB', 'MB', 'GB', 'TB'); + + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + + // Uncomment one of the following alternatives + $bytes /= 1024 ** $pow; + // $bytes /= (1 << (10 * $pow)); + + return round($bytes, $precision) . ' ' . $units[$pow]; + } + + /** + * Parse a readable file size and return a value in bytes + * + * @param string|int|float $size + * @return int + */ + public static function parseSize($size) + { + $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); + $size = (float)preg_replace('/[^0-9\.]/', '', $size); + + if ($unit) { + $size *= 1024 ** stripos('bkmgtpezy', $unit[0]); + } + + return (int)abs(round($size)); + } + + /** + * Multibyte-safe Parse URL function + * + * @param string $url + * @return array + * @throws InvalidArgumentException + */ + public static function multibyteParseUrl($url) + { + $enc_url = preg_replace_callback( + '%[^:/@?&=#]+%usD', + static function ($matches) { + return urlencode($matches[0]); + }, + $url + ); + + $parts = parse_url($enc_url); + + if ($parts === false) { + throw new InvalidArgumentException('Malformed URL: ' . $url); + } + + foreach ($parts as $name => $value) { + $parts[$name] = urldecode($value); + } + + return $parts; + } + + /** + * Process a string as markdown + * + * @param string $string + * @param bool $block Block or Line processing + * @param PageInterface|null $page + * @return string + * @throws Exception + */ + public static function processMarkdown($string, $block = true, $page = null) + { + $grav = Grav::instance(); + $page = $page ?? $grav['page'] ?? null; + $defaults = [ + 'markdown' => $grav['config']->get('system.pages.markdown', []), + 'images' => $grav['config']->get('system.images', []) + ]; + $extra = $defaults['markdown']['extra'] ?? false; + + $excerpts = new Excerpts($page, $defaults); + + // Initialize the preferred variant of Parsedown + if ($extra) { + $parsedown = new ParsedownExtra($excerpts); + } else { + $parsedown = new Parsedown($excerpts); + } + + if ($block) { + $string = $parsedown->text($string); + } else { + $string = $parsedown->line($string); + } + + return $string; + } + + /** + * Find the subnet of an ip with CIDR prefix size + * + * @param string $ip + * @param int $prefix + * @return string + */ + public static function getSubnet($ip, $prefix = 64) + { + if (!filter_var($ip, FILTER_VALIDATE_IP)) { + return $ip; + } + + // Packed representation of IP + $ip = (string)inet_pton($ip); + + // Maximum netmask length = same as packed address + $len = 8 * strlen($ip); + if ($prefix > $len) { + $prefix = $len; + } + + $mask = str_repeat('f', $prefix >> 2); + + switch ($prefix & 3) { + case 3: + $mask .= 'e'; + break; + case 2: + $mask .= 'c'; + break; + case 1: + $mask .= '8'; + break; + } + $mask = str_pad($mask, $len >> 2, '0'); + + // Packed representation of netmask + $mask = pack('H*', $mask); + // Bitwise - Take all bits that are both 1 to generate subnet + $subnet = inet_ntop($ip & $mask); + + return $subnet; + } + + /** + * Wrapper to ensure html, htm in the front of the supported page types + * + * @param array|null $defaults + * @return array + */ + public static function getSupportPageTypes(array $defaults = null) + { + $types = Grav::instance()['config']->get('system.pages.types', $defaults); + if (!is_array($types)) { + return []; + } + + // remove html/htm + $types = static::arrayRemoveValue($types, ['html', 'htm']); + + // put them back at the front + $types = array_merge(['html', 'htm'], $types); + + return $types; + } + + /** + * @param string $name + * @return bool + */ + public static function isDangerousFunction(string $name): bool + { + static $commandExecutionFunctions = [ + 'exec', + 'passthru', + 'system', + 'shell_exec', + 'popen', + 'proc_open', + 'pcntl_exec', + ]; + + static $codeExecutionFunctions = [ + 'assert', + 'preg_replace', + 'create_function', + 'include', + 'include_once', + 'require', + 'require_once' + ]; + + static $callbackFunctions = [ + 'ob_start' => 0, + 'array_diff_uassoc' => -1, + 'array_diff_ukey' => -1, + 'array_filter' => 1, + 'array_intersect_uassoc' => -1, + 'array_intersect_ukey' => -1, + 'array_map' => 0, + 'array_reduce' => 1, + 'array_udiff_assoc' => -1, + 'array_udiff_uassoc' => [-1, -2], + 'array_udiff' => -1, + 'array_uintersect_assoc' => -1, + 'array_uintersect_uassoc' => [-1, -2], + 'array_uintersect' => -1, + 'array_walk_recursive' => 1, + 'array_walk' => 1, + 'assert_options' => 1, + 'uasort' => 1, + 'uksort' => 1, + 'usort' => 1, + 'preg_replace_callback' => 1, + 'spl_autoload_register' => 0, + 'iterator_apply' => 1, + 'call_user_func' => 0, + 'call_user_func_array' => 0, + 'register_shutdown_function' => 0, + 'register_tick_function' => 0, + 'set_error_handler' => 0, + 'set_exception_handler' => 0, + 'session_set_save_handler' => [0, 1, 2, 3, 4, 5], + 'sqlite_create_aggregate' => [2, 3], + 'sqlite_create_function' => 2, + ]; + + static $informationDiscosureFunctions = [ + 'phpinfo', + 'posix_mkfifo', + 'posix_getlogin', + 'posix_ttyname', + 'getenv', + 'get_current_user', + 'proc_get_status', + 'get_cfg_var', + 'disk_free_space', + 'disk_total_space', + 'diskfreespace', + 'getcwd', + 'getlastmo', + 'getmygid', + 'getmyinode', + 'getmypid', + 'getmyuid' + ]; + + static $otherFunctions = [ + 'extract', + 'parse_str', + 'putenv', + 'ini_set', + 'mail', + 'header', + 'proc_nice', + 'proc_terminate', + 'proc_close', + 'pfsockopen', + 'fsockopen', + 'apache_child_terminate', + 'posix_kill', + 'posix_mkfifo', + 'posix_setpgid', + 'posix_setsid', + 'posix_setuid', + ]; + + if (in_array($name, $commandExecutionFunctions)) { + return true; + } + + if (in_array($name, $codeExecutionFunctions)) { + return true; + } + + if (isset($callbackFunctions[$name])) { + return true; + } + + if (in_array($name, $informationDiscosureFunctions)) { + return true; + } + + if (in_array($name, $otherFunctions)) { + return true; + } + + return static::isFilesystemFunction($name); + } + + /** + * @param string $name + * @return bool + */ + public static function isFilesystemFunction(string $name): bool + { + static $fileWriteFunctions = [ + 'fopen', + 'tmpfile', + 'bzopen', + 'gzopen', + // write to filesystem (partially in combination with reading) + 'chgrp', + 'chmod', + 'chown', + 'copy', + 'file_put_contents', + 'lchgrp', + 'lchown', + 'link', + 'mkdir', + 'move_uploaded_file', + 'rename', + 'rmdir', + 'symlink', + 'tempnam', + 'touch', + 'unlink', + 'imagepng', + 'imagewbmp', + 'image2wbmp', + 'imagejpeg', + 'imagexbm', + 'imagegif', + 'imagegd', + 'imagegd2', + 'iptcembed', + 'ftp_get', + 'ftp_nb_get', + ]; + + static $fileContentFunctions = [ + 'file_get_contents', + 'file', + 'filegroup', + 'fileinode', + 'fileowner', + 'fileperms', + 'glob', + 'is_executable', + 'is_uploaded_file', + 'parse_ini_file', + 'readfile', + 'readlink', + 'realpath', + 'gzfile', + 'readgzfile', + 'stat', + 'imagecreatefromgif', + 'imagecreatefromjpeg', + 'imagecreatefrompng', + 'imagecreatefromwbmp', + 'imagecreatefromxbm', + 'imagecreatefromxpm', + 'ftp_put', + 'ftp_nb_put', + 'hash_update_file', + 'highlight_file', + 'show_source', + 'php_strip_whitespace', + ]; + + static $filesystemFunctions = [ + // read from filesystem + 'file_exists', + 'fileatime', + 'filectime', + 'filemtime', + 'filesize', + 'filetype', + 'is_dir', + 'is_file', + 'is_link', + 'is_readable', + 'is_writable', + 'is_writeable', + 'linkinfo', + 'lstat', + //'pathinfo', + 'getimagesize', + 'exif_read_data', + 'read_exif_data', + 'exif_thumbnail', + 'exif_imagetype', + 'hash_file', + 'hash_hmac_file', + 'md5_file', + 'sha1_file', + 'get_meta_tags', + ]; + + if (in_array($name, $fileWriteFunctions)) { + return true; + } + + if (in_array($name, $fileContentFunctions)) { + return true; + } + + if (in_array($name, $filesystemFunctions)) { + return true; + } + + return false; + } +} diff --git a/system/src/Grav/Common/Yaml.php b/system/src/Grav/Common/Yaml.php new file mode 100644 index 0000000..330b6c3 --- /dev/null +++ b/system/src/Grav/Common/Yaml.php @@ -0,0 +1,65 @@ +decode($data); + } + + /** + * @param array $data + * @param int|null $inline + * @param int|null $indent + * @return string + */ + public static function dump($data, $inline = null, $indent = null) + { + if (null === static::$yaml) { + static::init(); + } + + return static::$yaml->encode($data, $inline, $indent); + } + + /** + * @return void + */ + protected static function init() + { + $config = [ + 'inline' => 5, + 'indent' => 2, + 'native' => true, + 'compat' => true + ]; + + static::$yaml = new YamlFormatter($config); + } +} diff --git a/system/src/Grav/Console/Application/Application.php b/system/src/Grav/Console/Application/Application.php new file mode 100644 index 0000000..21cea87 --- /dev/null +++ b/system/src/Grav/Console/Application/Application.php @@ -0,0 +1,106 @@ +environment = $input->getOption('env'); + $this->language = $input->getOption('lang') ?? $this->language; + $this->init(); + + return parent::getCommandName($input); + } + + /** + * @return void + */ + protected function init(): void + { + if ($this->initialized) { + return; + } + + $this->initialized = true; + + $grav = Grav::instance(); + $grav->setup($this->environment); + } + + /** + * Add global a --env option. + * + * @return InputDefinition + */ + protected function getDefaultInputDefinition(): InputDefinition + { + $inputDefinition = parent::getDefaultInputDefinition(); + $inputDefinition->addOption( + new InputOption( + 'env', + null, + InputOption::VALUE_OPTIONAL, + 'Use environment configuration (defaults to localhost)' + ) + ); + $inputDefinition->addOption( + new InputOption( + 'lang', + null, + InputOption::VALUE_OPTIONAL, + 'Language to be used (defaults to en)' + ) + ); + + return $inputDefinition; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return void + */ + protected function configureIO(InputInterface $input, OutputInterface $output) + { + $formatter = $output->getFormatter(); + $formatter->setStyle('normal', new OutputFormatterStyle('white')); + $formatter->setStyle('yellow', new OutputFormatterStyle('yellow', null, ['bold'])); + $formatter->setStyle('red', new OutputFormatterStyle('red', null, ['bold'])); + $formatter->setStyle('cyan', new OutputFormatterStyle('cyan', null, ['bold'])); + $formatter->setStyle('green', new OutputFormatterStyle('green', null, ['bold'])); + $formatter->setStyle('magenta', new OutputFormatterStyle('magenta', null, ['bold'])); + $formatter->setStyle('white', new OutputFormatterStyle('white', null, ['bold'])); + + parent::configureIO($input, $output); + } +} diff --git a/system/src/Grav/Console/Application/CommandLoader/PluginCommandLoader.php b/system/src/Grav/Console/Application/CommandLoader/PluginCommandLoader.php new file mode 100644 index 0000000..9b7b568 --- /dev/null +++ b/system/src/Grav/Console/Application/CommandLoader/PluginCommandLoader.php @@ -0,0 +1,95 @@ +commands = []; + + try { + $path = "plugins://{$name}/cli"; + $pattern = '([A-Z]\w+Command\.php)'; + + $commands = is_dir($path) ? Folder::all($path, ['compare' => 'Filename', 'pattern' => '/' . $pattern . '$/usm', 'levels' => 1]) : []; + } catch (RuntimeException $e) { + throw new RuntimeException("Failed to load console commands for plugin {$name}"); + } + + $grav = Grav::instance(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + foreach ($commands as $command_path) { + $full_path = $locator->findResource("plugins://{$name}/cli/{$command_path}"); + require_once $full_path; + + $command_class = 'Grav\Plugin\Console\\' . preg_replace('/.php$/', '', $command_path); + if (class_exists($command_class)) { + $command = new $command_class(); + if ($command instanceof Command) { + $this->commands[$command->getName()] = $command; + } + } + } + } + + /** + * @param string $name + * @return Command + */ + public function get($name): Command + { + $command = $this->commands[$name] ?? null; + if (null === $command) { + throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name)); + } + + return $command; + } + + /** + * @param string $name + * @return bool + */ + public function has($name): bool + { + return isset($this->commands[$name]); + } + + /** + * @return string[] + */ + public function getNames(): array + { + return array_keys($this->commands); + } +} diff --git a/system/src/Grav/Console/Application/GpmApplication.php b/system/src/Grav/Console/Application/GpmApplication.php new file mode 100644 index 0000000..df383f2 --- /dev/null +++ b/system/src/Grav/Console/Application/GpmApplication.php @@ -0,0 +1,42 @@ +addCommands([ + new IndexCommand(), + new VersionCommand(), + new InfoCommand(), + new InstallCommand(), + new UninstallCommand(), + new UpdateCommand(), + new SelfupgradeCommand(), + new DirectInstallCommand(), + ]); + } +} diff --git a/system/src/Grav/Console/Application/GravApplication.php b/system/src/Grav/Console/Application/GravApplication.php new file mode 100644 index 0000000..739ca39 --- /dev/null +++ b/system/src/Grav/Console/Application/GravApplication.php @@ -0,0 +1,52 @@ +addCommands([ + new InstallCommand(), + new ComposerCommand(), + new SandboxCommand(), + new CleanCommand(), + new ClearCacheCommand(), + new BackupCommand(), + new NewProjectCommand(), + new SchedulerCommand(), + new SecurityCommand(), + new LogViewerCommand(), + new YamlLinterCommand(), + new ServerCommand(), + new PageSystemValidatorCommand(), + ]); + } +} diff --git a/system/src/Grav/Console/Application/PluginApplication.php b/system/src/Grav/Console/Application/PluginApplication.php new file mode 100644 index 0000000..75e9eb7 --- /dev/null +++ b/system/src/Grav/Console/Application/PluginApplication.php @@ -0,0 +1,116 @@ +addCommands([ + new PluginListCommand(), + ]); + } + + /** + * @param string $pluginName + * @return void + */ + public function setPluginName(string $pluginName): void + { + $this->pluginName = $pluginName; + } + + /** + * @return string + */ + public function getPluginName(): string + { + return $this->pluginName; + } + + /** + * @param InputInterface|null $input + * @param OutputInterface|null $output + * @return int + * @throws Throwable + */ + public function run(InputInterface $input = null, OutputInterface $output = null): int + { + if (null === $input) { + $argv = $_SERVER['argv'] ?? []; + + $bin = array_shift($argv); + $this->pluginName = array_shift($argv); + $argv = array_merge([$bin], $argv); + + $input = new ArgvInput($argv); + } + + return parent::run($input, $output); + } + + /** + * @return void + */ + protected function init(): void + { + if ($this->initialized) { + return; + } + + parent::init(); + + if (null === $this->pluginName) { + $this->setDefaultCommand('plugins:list'); + + return; + } + + $grav = Grav::instance(); + $grav->initializeCli(); + + /** @var Plugins $plugins */ + $plugins = $grav['plugins']; + + $plugin = $this->pluginName ? $plugins::get($this->pluginName) : null; + if (null === $plugin) { + throw new NamespaceNotFoundException("Plugin \"{$this->pluginName}\" is not installed."); + } + if (!$plugin->enabled) { + throw new NamespaceNotFoundException("Plugin \"{$this->pluginName}\" is not enabled."); + } + + $this->setCommandLoader(new PluginCommandLoader($this->pluginName)); + } +} diff --git a/system/src/Grav/Console/Cli/BackupCommand.php b/system/src/Grav/Console/Cli/BackupCommand.php new file mode 100644 index 0000000..a8a025d --- /dev/null +++ b/system/src/Grav/Console/Cli/BackupCommand.php @@ -0,0 +1,138 @@ +setName('backup') + ->addArgument( + 'id', + InputArgument::OPTIONAL, + 'The ID of the backup profile to perform without prompting' + ) + ->setDescription('Creates a backup of the Grav instance') + ->setHelp('The backup creates a zipped backup.'); + + $this->source = getcwd(); + } + + /** + * @return int + */ + protected function serve(): int + { + $this->initializeGrav(); + + $input = $this->getInput(); + $io = $this->getIO(); + + $io->title('Grav Backup'); + + if (!class_exists(ZipArchive::class)) { + $io->error('php-zip extension needs to be enabled!'); + return 1; + } + + ProgressBar::setFormatDefinition('zip', 'Archiving %current% files [%bar%] %percent:3s%% %elapsed:6s% %message%'); + + $this->progress = new ProgressBar($this->output, 100); + $this->progress->setFormat('zip'); + + + /** @var Backups $backups */ + $backups = Grav::instance()['backups']; + $backups_list = $backups::getBackupProfiles(); + $backups_names = $backups->getBackupNames(); + + $id = null; + + $inline_id = $input->getArgument('id'); + if (null !== $inline_id && is_numeric($inline_id)) { + $id = $inline_id; + } + + if (null === $id) { + if (count($backups_list) > 1) { + $question = new ChoiceQuestion( + 'Choose a backup?', + $backups_names, + 0 + ); + $question->setErrorMessage('Option %s is invalid.'); + $backup_name = $io->askQuestion($question); + $id = array_search($backup_name, $backups_names, true); + + $io->newLine(); + $io->note('Selected backup: ' . $backup_name); + } else { + $id = 0; + } + } + + $backup = $backups::backup($id, function($args) { $this->outputProgress($args); }); + + $io->newline(2); + $io->success('Backup Successfully Created: ' . $backup); + + return 0; + } + + /** + * @param array $args + * @return void + */ + public function outputProgress(array $args): void + { + switch ($args['type']) { + case 'count': + $steps = $args['steps']; + $freq = (int)($steps > 100 ? round($steps / 100) : $steps); + $this->progress->setMaxSteps($steps); + $this->progress->setRedrawFrequency($freq); + $this->progress->setMessage('Adding files...'); + break; + case 'message': + $this->progress->setMessage($args['message']); + $this->progress->display(); + break; + case 'progress': + if (isset($args['complete']) && $args['complete']) { + $this->progress->finish(); + } else { + $this->progress->advance(); + } + break; + } + } +} diff --git a/system/src/Grav/Console/Cli/CleanCommand.php b/system/src/Grav/Console/Cli/CleanCommand.php new file mode 100644 index 0000000..84b46d6 --- /dev/null +++ b/system/src/Grav/Console/Cli/CleanCommand.php @@ -0,0 +1,411 @@ +setName('clean') + ->setDescription('Handles cleaning chores for Grav distribution') + ->setHelp('The clean clean extraneous folders and data'); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->setupConsole($input, $output); + + return $this->cleanPaths() ? 0 : 1; + } + + /** + * @return bool + */ + private function cleanPaths(): bool + { + $success = true; + + $this->io->writeln(''); + $this->io->writeln('DELETING'); + $anything = false; + foreach ($this->paths_to_remove as $path) { + $path = GRAV_ROOT . DS . $path; + try { + if (is_dir($path) && Folder::delete($path)) { + $anything = true; + $this->io->writeln('dir: ' . $path); + } elseif (is_file($path) && @unlink($path)) { + $anything = true; + $this->io->writeln('file: ' . $path); + } + } catch (\Exception $e) { + $success = false; + $this->io->error(sprintf('Failed to delete %s: %s', $path, $e->getMessage())); + } + } + if (!$anything) { + $this->io->writeln(''); + $this->io->writeln('Nothing to clean...'); + } + + return $success; + } + + /** + * Set colors style definition for the formatter. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return void + */ + public function setupConsole(InputInterface $input, OutputInterface $output): void + { + $this->input = $input; + $this->io = new SymfonyStyle($input, $output); + + $this->io->getFormatter()->setStyle('normal', new OutputFormatterStyle('white')); + $this->io->getFormatter()->setStyle('yellow', new OutputFormatterStyle('yellow', null, ['bold'])); + $this->io->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, ['bold'])); + $this->io->getFormatter()->setStyle('cyan', new OutputFormatterStyle('cyan', null, ['bold'])); + $this->io->getFormatter()->setStyle('green', new OutputFormatterStyle('green', null, ['bold'])); + $this->io->getFormatter()->setStyle('magenta', new OutputFormatterStyle('magenta', null, ['bold'])); + $this->io->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, ['bold'])); + } +} diff --git a/system/src/Grav/Console/Cli/ClearCacheCommand.php b/system/src/Grav/Console/Cli/ClearCacheCommand.php new file mode 100644 index 0000000..daed2a5 --- /dev/null +++ b/system/src/Grav/Console/Cli/ClearCacheCommand.php @@ -0,0 +1,104 @@ +setName('cache') + ->setAliases(['clearcache', 'cache-clear']) + ->setDescription('Clears Grav cache') + ->addOption('invalidate', null, InputOption::VALUE_NONE, 'Invalidate cache, but do not remove any files') + ->addOption('purge', null, InputOption::VALUE_NONE, 'If set purge old caches') + ->addOption('all', null, InputOption::VALUE_NONE, 'If set will remove all including compiled, twig, doctrine caches') + ->addOption('assets-only', null, InputOption::VALUE_NONE, 'If set will remove only assets/*') + ->addOption('images-only', null, InputOption::VALUE_NONE, 'If set will remove only images/*') + ->addOption('cache-only', null, InputOption::VALUE_NONE, 'If set will remove only cache/*') + ->addOption('tmp-only', null, InputOption::VALUE_NONE, 'If set will remove only tmp/*') + + ->setHelp('The cache command allows you to interact with Grav cache'); + } + + /** + * @return int + */ + protected function serve(): int + { + // Old versions of Grav called this command after grav upgrade. + // We need make this command to work with older GravCommand instance: + if (!method_exists($this, 'initializePlugins')) { + Cache::clearCache('all'); + + return 0; + } + + $this->initializePlugins(); + $this->cleanPaths(); + + return 0; + } + + /** + * loops over the array of paths and deletes the files/folders + * + * @return void + */ + private function cleanPaths(): void + { + $input = $this->getInput(); + $io = $this->getIO(); + + $io->newLine(); + + if ($input->getOption('purge')) { + $io->writeln('Purging old cache'); + $io->newLine(); + + $msg = Cache::purgeJob(); + $io->writeln($msg); + } else { + $io->writeln('Clearing cache'); + $io->newLine(); + + if ($input->getOption('all')) { + $remove = 'all'; + } elseif ($input->getOption('assets-only')) { + $remove = 'assets-only'; + } elseif ($input->getOption('images-only')) { + $remove = 'images-only'; + } elseif ($input->getOption('cache-only')) { + $remove = 'cache-only'; + } elseif ($input->getOption('tmp-only')) { + $remove = 'tmp-only'; + } elseif ($input->getOption('invalidate')) { + $remove = 'invalidate'; + } else { + $remove = 'standard'; + } + + foreach (Cache::clearCache($remove) as $result) { + $io->writeln($result); + } + } + } +} diff --git a/system/src/Grav/Console/Cli/ComposerCommand.php b/system/src/Grav/Console/Cli/ComposerCommand.php new file mode 100644 index 0000000..5075d1d --- /dev/null +++ b/system/src/Grav/Console/Cli/ComposerCommand.php @@ -0,0 +1,64 @@ +setName('composer') + ->addOption( + 'install', + 'i', + InputOption::VALUE_NONE, + 'install the dependencies' + ) + ->addOption( + 'update', + 'u', + InputOption::VALUE_NONE, + 'update the dependencies' + ) + ->setDescription('Updates the composer vendor dependencies needed by Grav.') + ->setHelp('The composer command updates the composer vendor dependencies needed by Grav'); + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + $action = $input->getOption('install') ? 'install' : ($input->getOption('update') ? 'update' : 'install'); + + if ($input->getOption('install')) { + $action = 'install'; + } + + // Updates composer first + $io->writeln("\nInstalling vendor dependencies"); + $io->writeln($this->composerUpdate(GRAV_ROOT, $action)); + + return 0; + } +} diff --git a/system/src/Grav/Console/Cli/InstallCommand.php b/system/src/Grav/Console/Cli/InstallCommand.php new file mode 100644 index 0000000..22258be --- /dev/null +++ b/system/src/Grav/Console/Cli/InstallCommand.php @@ -0,0 +1,302 @@ +setName('install') + ->addOption( + 'symlink', + 's', + InputOption::VALUE_NONE, + 'Symlink the required bits' + ) + ->addOption( + 'plugin', + 'p', + InputOption::VALUE_REQUIRED, + 'Install plugin (symlink)' + ) + ->addOption( + 'theme', + 't', + InputOption::VALUE_REQUIRED, + 'Install theme (symlink)' + ) + ->addArgument( + 'destination', + InputArgument::OPTIONAL, + 'Where to install the required bits (default to current project)' + ) + ->setDescription('Installs the dependencies needed by Grav. Optionally can create symbolic links') + ->setHelp('The install command installs the dependencies needed by Grav. Optionally can create symbolic links'); + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + $dependencies_file = '.dependencies'; + $this->destination = $input->getArgument('destination') ?: GRAV_WEBROOT; + + // fix trailing slash + $this->destination = rtrim($this->destination, DS) . DS; + $this->user_path = $this->destination . GRAV_USER_PATH . DS; + if ($local_config_file = $this->loadLocalConfig()) { + $io->writeln('Read local config from ' . $local_config_file . ''); + } + + // Look for dependencies file in ROOT and USER dir + if (file_exists($this->user_path . $dependencies_file)) { + $file = YamlFile::instance($this->user_path . $dependencies_file); + } elseif (file_exists($this->destination . $dependencies_file)) { + $file = YamlFile::instance($this->destination . $dependencies_file); + } else { + $io->writeln('ERROR Missing .dependencies file in user/ folder'); + if ($input->getArgument('destination')) { + $io->writeln('HINT Are you trying to install a plugin or a theme? Make sure you use bin/gpm install , not bin/grav install. This command is only used to install Grav skeletons.'); + } else { + $io->writeln('HINT Are you trying to install Grav? Grav is already installed. You need to run this command only if you download a skeleton from GitHub directly.'); + } + + return 1; + } + + $this->config = $file->content(); + $file->free(); + + // If no config, fail. + if (!$this->config) { + $io->writeln('ERROR invalid YAML in ' . $dependencies_file); + + return 1; + } + + $plugin = $input->getOption('plugin'); + $theme = $input->getOption('theme'); + $name = $plugin ?? $theme; + $symlink = $name || $input->getOption('symlink'); + + if (!$symlink) { + // Updates composer first + $io->writeln("\nInstalling vendor dependencies"); + $io->writeln($this->composerUpdate(GRAV_ROOT, 'install')); + + $error = $this->gitclone(); + } else { + $type = $name ? ($plugin ? 'plugin' : 'theme') : null; + + $error = $this->symlink($name, $type); + } + + return $error; + } + + /** + * Clones from Git + * + * @return int + */ + private function gitclone(): int + { + $io = $this->getIO(); + + $io->newLine(); + $io->writeln('Cloning Bits'); + $io->writeln('============'); + $io->newLine(); + + $error = 0; + $this->destination = rtrim($this->destination, DS); + foreach ($this->config['git'] as $repo => $data) { + $path = $this->destination . DS . $data['path']; + if (!file_exists($path)) { + exec('cd "' . $this->destination . '" && git clone -b ' . $data['branch'] . ' --depth 1 ' . $data['url'] . ' ' . $data['path'], $output, $return); + + if (!$return) { + $io->writeln('SUCCESS cloned ' . $data['url'] . ' -> ' . $path . ''); + } else { + $io->writeln('ERROR cloning ' . $data['url']); + $error = 1; + } + + $io->newLine(); + } else { + $io->writeln('' . $path . ' already exists, skipping...'); + $io->newLine(); + } + } + + return $error; + } + + /** + * Symlinks + * + * @param string|null $name + * @param string|null $type + * @return int + */ + private function symlink(string $name = null, string $type = null): int + { + $io = $this->getIO(); + + $io->newLine(); + $io->writeln('Symlinking Bits'); + $io->writeln('==============='); + $io->newLine(); + + if (!$this->local_config) { + $io->writeln('No local configuration available, aborting...'); + $io->newLine(); + + return 1; + } + + $error = 0; + $this->destination = rtrim($this->destination, DS); + + if ($name) { + $src = "grav-{$type}-{$name}"; + $links = [ + $name => [ + 'scm' => 'github', // TODO: make configurable + 'src' => $src, + 'path' => "user/{$type}s/{$name}" + ] + ]; + } else { + $links = $this->config['links']; + } + + foreach ($links as $name => $data) { + $scm = $data['scm'] ?? null; + $src = $data['src'] ?? null; + $path = $data['path'] ?? null; + if (!isset($scm, $src, $path)) { + $io->writeln("Dependency '$name' has broken configuration, skipping..."); + $io->newLine(); + $error = 1; + + continue; + } + + $locations = (array) $this->local_config["{$scm}_repos"]; + $to = $this->destination . DS . $path; + + $from = null; + foreach ($locations as $location) { + $test = rtrim($location, '\\/') . DS . $src; + if (file_exists($test)) { + $from = $test; + continue; + } + } + + if (is_link($to) && !realpath($to)) { + $io->writeln('Removed broken symlink '. $path .''); + unlink($to); + } + if (null === $from) { + $io->writeln('source for ' . $src . ' does not exists, skipping...'); + $io->newLine(); + $error = 1; + } elseif (!file_exists($to)) { + $error = $this->addSymlinks($from, $to, ['name' => $name, 'src' => $src, 'path' => $path]); + $io->newLine(); + } else { + $io->writeln('destination: ' . $path . ' already exists, skipping...'); + $io->newLine(); + } + } + + return $error; + } + + private function addSymlinks(string $from, string $to, array $options): int + { + $io = $this->getIO(); + + $hebe = $this->readHebe($from); + if (null === $hebe) { + symlink($from, $to); + + $io->writeln('SUCCESS symlinked ' . $options['src'] . ' -> ' . $options['path'] . ''); + } else { + $to = GRAV_ROOT; + $name = $options['name']; + $io->writeln("Processing {$name}"); + foreach ($hebe as $section => $symlinks) { + foreach ($symlinks as $symlink) { + $src = trim($symlink['source'], '/'); + $dst = trim($symlink['destination'], '/'); + $s = "{$from}/{$src}"; + $d = "{$to}/{$dst}"; + + if (is_link($d) && !realpath($d)) { + unlink($d); + $io->writeln(' Removed broken symlink '. $dst .''); + } + if (!file_exists($d)) { + symlink($s, $d); + $io->writeln(' symlinked ' . $src . ' -> ' . $dst . ''); + } + } + } + $io->writeln('SUCCESS'); + } + + return 0; + } + + private function readHebe(string $folder): ?array + { + $filename = "{$folder}/hebe.json"; + if (!is_file($filename)) { + return null; + } + + $formatter = new JsonFormatter(); + $file = new JsonFile($filename, $formatter); + $hebe = $file->load(); + $paths = $hebe['platforms']['grav']['nodes'] ?? null; + + return is_array($paths) ? $paths : null; + } +} diff --git a/system/src/Grav/Console/Cli/LogViewerCommand.php b/system/src/Grav/Console/Cli/LogViewerCommand.php new file mode 100644 index 0000000..d3924f8 --- /dev/null +++ b/system/src/Grav/Console/Cli/LogViewerCommand.php @@ -0,0 +1,96 @@ +setName('logviewer') + ->addOption( + 'file', + 'f', + InputOption::VALUE_OPTIONAL, + 'custom log file location (default = grav.log)' + ) + ->addOption( + 'lines', + 'l', + InputOption::VALUE_OPTIONAL, + 'number of lines (default = 10)' + ) + ->setDescription('Display the last few entries of Grav log') + ->setHelp('Display the last few entries of Grav log'); + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + $file = $input->getOption('file') ?? 'grav.log'; + $lines = $input->getOption('lines') ?? 20; + $verbose = $input->getOption('verbose') ?? false; + + $io->title('Log Viewer'); + + $io->writeln(sprintf('viewing last %s entries in %s', $lines, $file)); + $io->newLine(); + + $viewer = new LogViewer(); + + $grav = Grav::instance(); + + $logfile = $grav['locator']->findResource('log://' . $file); + if (!$logfile) { + $io->error('cannot find the log file: logs/' . $file); + + return 1; + } + + $rows = $viewer->objectTail($logfile, $lines, true); + foreach ($rows as $log) { + $date = $log['date']; + $level_color = LogViewer::levelColor($log['level']); + + if ($date instanceof DateTime) { + $output = "{$log['date']->format('Y-m-d h:i:s')} [<{$level_color}>{$log['level']}]"; + if ($log['trace'] && $verbose) { + $output .= " {$log['message']}\n"; + foreach ((array) $log['trace'] as $index => $tracerow) { + $output .= "{$index}${tracerow}\n"; + } + } else { + $output .= " {$log['message']}"; + } + $io->writeln($output); + } + } + + return 0; + } +} diff --git a/system/src/Grav/Console/Cli/NewProjectCommand.php b/system/src/Grav/Console/Cli/NewProjectCommand.php new file mode 100644 index 0000000..d67cb1c --- /dev/null +++ b/system/src/Grav/Console/Cli/NewProjectCommand.php @@ -0,0 +1,75 @@ +setName('new-project') + ->setAliases(['newproject']) + ->addArgument( + 'destination', + InputArgument::REQUIRED, + 'The destination directory of your new Grav project' + ) + ->addOption( + 'symlink', + 's', + InputOption::VALUE_NONE, + 'Symlink the required bits' + ) + ->setDescription('Creates a new Grav project with all the dependencies installed') + ->setHelp("The new-project command is a combination of the `setup` and `install` commands.\nCreates a new Grav instance and performs the installation of all the required dependencies."); + } + + /** + * @return int + */ + protected function serve(): int + { + $io = $this->getIO(); + + $sandboxCommand = $this->getApplication()->find('sandbox'); + $installCommand = $this->getApplication()->find('install'); + + $sandboxArguments = new ArrayInput([ + 'command' => 'sandbox', + 'destination' => $this->input->getArgument('destination'), + '-s' => $this->input->getOption('symlink') + ]); + + $installArguments = new ArrayInput([ + 'command' => 'install', + 'destination' => $this->input->getArgument('destination'), + '-s' => $this->input->getOption('symlink') + ]); + + $error = $sandboxCommand->run($sandboxArguments, $io); + if ($error === 0) { + $error = $installCommand->run($installArguments, $io); + } + + return $error; + } +} diff --git a/system/src/Grav/Console/Cli/PageSystemValidatorCommand.php b/system/src/Grav/Console/Cli/PageSystemValidatorCommand.php new file mode 100644 index 0000000..4d234d7 --- /dev/null +++ b/system/src/Grav/Console/Cli/PageSystemValidatorCommand.php @@ -0,0 +1,299 @@ + [[]], + 'summary' => [[], [200], [200, true]], + 'content' => [[]], + 'getRawContent' => [[]], + 'rawMarkdown' => [[]], + 'value' => [['content'], ['route'], ['order'], ['ordering'], ['folder'], ['slug'], ['name'], /*['frontmatter'],*/ ['header.menu'], ['header.slug']], + 'title' => [[]], + 'menu' => [[]], + 'visible' => [[]], + 'published' => [[]], + 'publishDate' => [[]], + 'unpublishDate' => [[]], + 'process' => [[]], + 'slug' => [[]], + 'order' => [[]], + //'id' => [[]], + 'modified' => [[]], + 'lastModified' => [[]], + 'folder' => [[]], + 'date' => [[]], + 'dateformat' => [[]], + 'taxonomy' => [[]], + 'shouldProcess' => [['twig'], ['markdown']], + 'isPage' => [[]], + 'isDir' => [[]], + 'exists' => [[]], + + // Forms + 'forms' => [[]], + + // Routing + 'urlExtension' => [[]], + 'routable' => [[]], + 'link' => [[], [false], [true]], + 'permalink' => [[]], + 'canonical' => [[], [false], [true]], + 'url' => [[], [true], [true, true], [true, true, false], [false, false, true, false]], + 'route' => [[]], + 'rawRoute' => [[]], + 'routeAliases' => [[]], + 'routeCanonical' => [[]], + 'redirect' => [[]], + 'relativePagePath' => [[]], + 'path' => [[]], + //'folder' => [[]], + 'parent' => [[]], + 'topParent' => [[]], + 'currentPosition' => [[]], + 'active' => [[]], + 'activeChild' => [[]], + 'home' => [[]], + 'root' => [[]], + + // Translations + 'translatedLanguages' => [[], [false], [true]], + 'untranslatedLanguages' => [[], [false], [true]], + 'language' => [[]], + + // Legacy + 'raw' => [[]], + 'frontmatter' => [[]], + 'httpResponseCode' => [[]], + 'httpHeaders' => [[]], + 'blueprintName' => [[]], + 'name' => [[]], + 'childType' => [[]], + 'template' => [[]], + 'templateFormat' => [[]], + 'extension' => [[]], + 'expires' => [[]], + 'cacheControl' => [[]], + 'ssl' => [[]], + 'metadata' => [[]], + 'eTag' => [[]], + 'filePath' => [[]], + 'filePathClean' => [[]], + 'orderDir' => [[]], + 'orderBy' => [[]], + 'orderManual' => [[]], + 'maxCount' => [[]], + 'modular' => [[]], + 'modularTwig' => [[]], + //'children' => [[]], + 'isFirst' => [[]], + 'isLast' => [[]], + 'prevSibling' => [[]], + 'nextSibling' => [[]], + 'adjacentSibling' => [[]], + 'ancestor' => [[]], + //'inherited' => [[]], + //'inheritedField' => [[]], + 'find' => [['/']], + //'collection' => [[]], + //'evaluate' => [[]], + 'folderExists' => [[]], + //'getOriginal' => [[]], + //'getAction' => [[]], + ]; + + /** @var Grav */ + protected $grav; + + /** + * @return void + */ + protected function configure(): void + { + $this + ->setName('page-system-validator') + ->setDescription('Page validator can be used to compare site before/after update and when migrating to Flex Pages.') + ->addOption('record', 'r', InputOption::VALUE_NONE, 'Record results') + ->addOption('check', 'c', InputOption::VALUE_NONE, 'Compare site against previously recorded results') + ->setHelp('The page-system-validator command can be used to test the pages before and after upgrade'); + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + $this->setLanguage('en'); + $this->initializePages(); + + $io->newLine(); + + $this->grav = $grav = Grav::instance(); + + $grav->fireEvent('onPageInitialized', new Event(['page' => $grav['page']])); + + /** @var Config $config */ + $config = $grav['config']; + + if ($input->getOption('record')) { + $io->writeln('Pages: ' . $config->get('system.pages.type', 'page')); + + $io->writeln('Record tests'); + $io->newLine(); + + $results = $this->record(); + $file = $this->getFile('pages-old'); + $file->save($results); + + $io->writeln('Recorded tests to ' . $file->filename()); + } elseif ($input->getOption('check')) { + $io->writeln('Pages: ' . $config->get('system.pages.type', 'page')); + + $io->writeln('Run tests'); + $io->newLine(); + + $new = $this->record(); + $file = $this->getFile('pages-new'); + $file->save($new); + $io->writeln('Recorded tests to ' . $file->filename()); + + $file = $this->getFile('pages-old'); + $old = $file->content(); + + $results = $this->check($old, $new); + $file = $this->getFile('diff'); + $file->save($results); + $io->writeln('Recorded results to ' . $file->filename()); + } else { + $io->writeln('page-system-validator [-r|--record] [-c|--check]'); + } + $io->newLine(); + + return 0; + } + + /** + * @return array + */ + private function record(): array + { + $io = $this->getIO(); + + /** @var Pages $pages */ + $pages = $this->grav['pages']; + $all = $pages->all(); + + $results = []; + $results[''] = $this->recordRow($pages->root()); + foreach ($all as $path => $page) { + if (null === $page) { + $io->writeln('Error on page ' . $path . ''); + continue; + } + + $results[$page->rawRoute()] = $this->recordRow($page); + } + + return json_decode(json_encode($results), true); + } + + /** + * @param PageInterface $page + * @return array + */ + private function recordRow(PageInterface $page): array + { + $results = []; + + foreach ($this->tests as $method => $params) { + $params = $params ?: [[]]; + foreach ($params as $p) { + $result = $page->$method(...$p); + if (in_array($method, ['summary', 'content', 'getRawContent'], true)) { + $result = preg_replace('/name="(form-nonce|__unique_form_id__)" value="[^"]+"/', + 'name="\\1" value="DYNAMIC"', $result); + $result = preg_replace('`src=("|\'|")/images/./././././[^"]+\\1`', + 'src="\\1images/GENERATED\\1', $result); + $result = preg_replace('/\?\d{10}/', '?1234567890', $result); + } elseif ($method === 'httpHeaders' && isset($result['Expires'])) { + $result['Expires'] = 'Thu, 19 Sep 2019 13:10:24 GMT (REPLACED AS DYNAMIC)'; + } elseif ($result instanceof PageInterface) { + $result = $result->rawRoute(); + } elseif (is_object($result)) { + $result = json_decode(json_encode($result), true); + } + + $ps = []; + foreach ($p as $val) { + $ps[] = (string)var_export($val, true); + } + $pstr = implode(', ', $ps); + $call = "->{$method}({$pstr})"; + $results[$call] = $result; + } + } + + return $results; + } + + /** + * @param array $old + * @param array $new + * @return array + */ + private function check(array $old, array $new): array + { + $errors = []; + foreach ($old as $path => $page) { + if (!isset($new[$path])) { + $errors[$path] = 'PAGE REMOVED'; + continue; + } + foreach ($page as $method => $test) { + if (($new[$path][$method] ?? null) !== $test) { + $errors[$path][$method] = ['old' => $test, 'new' => $new[$path][$method]]; + } + } + } + + return $errors; + } + + /** + * @param string $name + * @return CompiledYamlFile + */ + private function getFile(string $name): CompiledYamlFile + { + return CompiledYamlFile::instance('cache://tests/' . $name . '.yaml'); + } +} diff --git a/system/src/Grav/Console/Cli/SandboxCommand.php b/system/src/Grav/Console/Cli/SandboxCommand.php new file mode 100644 index 0000000..d865b4a --- /dev/null +++ b/system/src/Grav/Console/Cli/SandboxCommand.php @@ -0,0 +1,342 @@ + '/.gitignore', + '/.editorconfig' => '/.editorconfig', + '/CHANGELOG.md' => '/CHANGELOG.md', + '/LICENSE.txt' => '/LICENSE.txt', + '/README.md' => '/README.md', + '/CONTRIBUTING.md' => '/CONTRIBUTING.md', + '/index.php' => '/index.php', + '/composer.json' => '/composer.json', + '/bin' => '/bin', + '/system' => '/system', + '/vendor' => '/vendor', + '/webserver-configs' => '/webserver-configs', + ]; + + /** @var string */ + protected $source; + /** @var string */ + protected $destination; + + /** + * @return void + */ + protected function configure(): void + { + $this + ->setName('sandbox') + ->setDescription('Setup of a base Grav system in your webroot, good for development, playing around or starting fresh') + ->addArgument( + 'destination', + InputArgument::REQUIRED, + 'The destination directory to symlink into' + ) + ->addOption( + 'symlink', + 's', + InputOption::VALUE_NONE, + 'Symlink the base grav system' + ) + ->setHelp("The sandbox command help create a development environment that can optionally use symbolic links to link the core of grav to the git cloned repository.\nGood for development, playing around or starting fresh"); + + $source = getcwd(); + if ($source === false) { + throw new RuntimeException('Internal Error'); + } + $this->source = $source; + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + + $this->destination = $input->getArgument('destination'); + + // Create Some core stuff if it doesn't exist + $error = $this->createDirectories(); + if ($error) { + return $error; + } + + // Copy files or create symlinks + $error = $input->getOption('symlink') ? $this->symlink() : $this->copy(); + if ($error) { + return $error; + } + + $error = $this->pages(); + if ($error) { + return $error; + } + + $error = $this->initFiles(); + if ($error) { + return $error; + } + + $error = $this->perms(); + if ($error) { + return $error; + } + + return 0; + } + + /** + * @return int + */ + private function createDirectories(): int + { + $io = $this->getIO(); + + $io->newLine(); + $io->writeln('Creating Directories'); + $dirs_created = false; + + if (!file_exists($this->destination)) { + Folder::create($this->destination); + } + + foreach ($this->directories as $dir) { + if (!file_exists($this->destination . $dir)) { + $dirs_created = true; + $io->writeln(' ' . $dir . ''); + Folder::create($this->destination . $dir); + } + } + + if (!$dirs_created) { + $io->writeln(' Directories already exist'); + } + + return 0; + } + + /** + * @return int + */ + private function copy(): int + { + $io = $this->getIO(); + + $io->newLine(); + $io->writeln('Copying Files'); + + + foreach ($this->mappings as $source => $target) { + if ((string)(int)$source === (string)$source) { + $source = $target; + } + + $from = $this->source . $source; + $to = $this->destination . $target; + + $io->writeln(' ' . $source . ' -> ' . $to); + @Folder::rcopy($from, $to); + } + + return 0; + } + + /** + * @return int + */ + private function symlink(): int + { + $io = $this->getIO(); + + $io->newLine(); + $io->writeln('Resetting Symbolic Links'); + + + foreach ($this->mappings as $source => $target) { + if ((string)(int)$source === (string)$source) { + $source = $target; + } + + $from = $this->source . $source; + $to = $this->destination . $target; + + $io->writeln(' ' . $source . ' -> ' . $to); + + if (is_dir($to)) { + @Folder::delete($to); + } else { + @unlink($to); + } + symlink($from, $to); + } + + return 0; + } + + /** + * @return int + */ + private function pages(): int + { + $io = $this->getIO(); + + $io->newLine(); + $io->writeln('Pages Initializing'); + + // get pages files and initialize if no pages exist + $pages_dir = $this->destination . '/user/pages'; + $pages_files = array_diff(scandir($pages_dir), ['..', '.']); + + if (count($pages_files) === 0) { + $destination = $this->source . '/user/pages'; + Folder::rcopy($destination, $pages_dir); + $io->writeln(' ' . $destination . ' -> Created'); + } + + return 0; + } + + /** + * @return int + */ + private function initFiles(): int + { + if (!$this->check()) { + return 1; + } + + $io = $this->getIO(); + $io->newLine(); + $io->writeln('File Initializing'); + $files_init = false; + + // Copy files if they do not exist + foreach ($this->files as $source => $target) { + if ((string)(int)$source === (string)$source) { + $source = $target; + } + + $from = $this->source . $source; + $to = $this->destination . $target; + + if (!file_exists($to)) { + $files_init = true; + copy($from, $to); + $io->writeln(' ' . $target . ' -> Created'); + } + } + + if (!$files_init) { + $io->writeln(' Files already exist'); + } + + return 0; + } + + /** + * @return int + */ + private function perms(): int + { + $io = $this->getIO(); + $io->newLine(); + $io->writeln('Permissions Initializing'); + + $dir_perms = 0755; + + $binaries = glob($this->destination . DS . 'bin' . DS . '*'); + + foreach ($binaries as $bin) { + chmod($bin, $dir_perms); + $io->writeln(' bin/' . basename($bin) . ' permissions reset to ' . decoct($dir_perms)); + } + + $io->newLine(); + + return 0; + } + + /** + * @return bool + */ + private function check(): bool + { + $success = true; + $io = $this->getIO(); + + if (!file_exists($this->destination)) { + $io->writeln(' file: ' . $this->destination . ' does not exist!'); + $success = false; + } + + foreach ($this->directories as $dir) { + if (!file_exists($this->destination . $dir)) { + $io->writeln(' directory: ' . $dir . ' does not exist!'); + $success = false; + } + } + + foreach ($this->mappings as $target => $link) { + if (!file_exists($this->destination . $target)) { + $io->writeln(' mappings: ' . $target . ' does not exist!'); + $success = false; + } + } + + if (!$success) { + $io->newLine(); + $io->writeln('install should be run with --symlink|--s to symlink first'); + } + + return $success; + } +} diff --git a/system/src/Grav/Console/Cli/SchedulerCommand.php b/system/src/Grav/Console/Cli/SchedulerCommand.php new file mode 100644 index 0000000..c5385aa --- /dev/null +++ b/system/src/Grav/Console/Cli/SchedulerCommand.php @@ -0,0 +1,225 @@ +setName('scheduler') + ->addOption( + 'install', + 'i', + InputOption::VALUE_NONE, + 'Show Install Command' + ) + ->addOption( + 'jobs', + 'j', + InputOption::VALUE_NONE, + 'Show Jobs Summary' + ) + ->addOption( + 'details', + 'd', + InputOption::VALUE_NONE, + 'Show Job Details' + ) + ->addOption( + 'run', + 'r', + InputOption::VALUE_OPTIONAL, + 'Force run all jobs or a specific job if you specify a specific Job ID', + false + ) + ->setDescription('Run the Grav Scheduler. Best when integrated with system cron') + ->setHelp("Running without any options will force the Scheduler to run through it's jobs and process them"); + } + + /** + * @return int + */ + protected function serve(): int + { + $this->initializePlugins(); + + $grav = Grav::instance(); + $grav['backups']->init(); + $this->initializePages(); + $this->initializeThemes(); + + /** @var Scheduler $scheduler */ + $scheduler = $grav['scheduler']; + $grav->fireEvent('onSchedulerInitialized', new Event(['scheduler' => $scheduler])); + + $this->setHelp('foo'); + + $input = $this->getInput(); + $io = $this->getIO(); + $error = 0; + + $run = $input->getOption('run'); + + if ($input->getOption('jobs')) { + // Show jobs list + + $jobs = $scheduler->getAllJobs(); + $job_states = (array)$scheduler->getJobStates()->content(); + $rows = []; + + $table = new Table($io); + $table->setStyle('box'); + $headers = ['Job ID', 'Command', 'Run At', 'Status', 'Last Run', 'State']; + + $io->title('Scheduler Jobs Listing'); + + foreach ($jobs as $job) { + $job_status = ucfirst($job_states[$job->getId()]['state'] ?? 'ready'); + $last_run = $job_states[$job->getId()]['last-run'] ?? 0; + $status = $job_status === 'Failure' ? "{$job_status}" : "{$job_status}"; + $state = $job->getEnabled() ? 'Enabled' : 'Disabled'; + $row = [ + $job->getId(), + "{$job->getCommand()}", + "{$job->getAt()}", + $status, + '' . ($last_run === 0 ? 'Never' : date('Y-m-d H:i', $last_run)) . '', + $state, + + ]; + $rows[] = $row; + } + + if (!empty($rows)) { + $table->setHeaders($headers); + $table->setRows($rows); + $table->render(); + } else { + $io->text('no jobs found...'); + } + + $io->newLine(); + $io->note('For error details run "bin/grav scheduler -d"'); + $io->newLine(); + } elseif ($input->getOption('details')) { + $jobs = $scheduler->getAllJobs(); + $job_states = (array)$scheduler->getJobStates()->content(); + + $io->title('Job Details'); + + $table = new Table($io); + $table->setStyle('box'); + $table->setHeaders(['Job ID', 'Last Run', 'Next Run', 'Errors']); + $rows = []; + + foreach ($jobs as $job) { + $job_state = $job_states[$job->getId()]; + $error = isset($job_state['error']) ? trim($job_state['error']) : false; + + /** @var CronExpression $expression */ + $expression = $job->getCronExpression(); + $next_run = $expression->getNextRunDate(); + + $row = []; + $row[] = $job->getId(); + if (!is_null($job_state['last-run'])) { + $row[] = '' . date('Y-m-d H:i', $job_state['last-run']) . ''; + } else { + $row[] = 'Never'; + } + $row[] = '' . $next_run->format('Y-m-d H:i') . ''; + + if ($error) { + $row[] = "{$error}"; + } else { + $row[] = 'None'; + } + $rows[] = $row; + } + + $table->setRows($rows); + $table->render(); + } elseif ($run !== false && $run !== null) { + $io->title('Force Run Job: ' . $run); + + $job = $scheduler->getJob($run); + + if ($job) { + $job->inForeground()->run(); + + if ($job->isSuccessful()) { + $io->success('Job ran successfully...'); + } else { + $error = 1; + $io->error('Job failed to run successfully...'); + } + + $output = $job->getOutput(); + + if ($output) { + $io->write($output); + } + } else { + $error = 1; + $io->error('Could not find a job with id: ' . $run); + } + } elseif ($input->getOption('install')) { + $io->title('Install Scheduler'); + + $verb = 'install'; + + if ($scheduler->isCrontabSetup()) { + $io->success('All Ready! You have already set up Grav\'s Scheduler in your crontab. You can validate this by running "crontab -l" to list your current crontab entries.'); + $verb = 'reinstall'; + } else { + $user = $scheduler->whoami(); + $error = 1; + $io->error('Can\'t find a crontab for ' . $user . '. You need to set up Grav\'s Scheduler in your crontab'); + } + if (!Utils::isWindows()) { + $io->note("To $verb, run the following command from your terminal:"); + $io->newLine(); + $io->text(trim($scheduler->getCronCommand())); + } else { + $io->note("To $verb, create a scheduled task in Windows."); + $io->text('Learn more at https://learn.getgrav.org/advanced/scheduler'); + } + } else { + // Run scheduler + $force = $run === null; + $scheduler->run(null, $force); + + if ($input->getOption('verbose')) { + $io->title('Running Scheduled Jobs'); + $io->text($scheduler->getVerboseOutput()); + } + } + + return $error; + } +} diff --git a/system/src/Grav/Console/Cli/SecurityCommand.php b/system/src/Grav/Console/Cli/SecurityCommand.php new file mode 100644 index 0000000..7f728b6 --- /dev/null +++ b/system/src/Grav/Console/Cli/SecurityCommand.php @@ -0,0 +1,102 @@ +setName('security') + ->setDescription('Capable of running various Security checks') + ->setHelp('The security runs various security checks on your Grav site'); + } + + /** + * @return int + */ + protected function serve(): int + { + $this->initializePages(); + + $io = $this->getIO(); + + /** @var Grav $grav */ + $grav = Grav::instance(); + $this->progress = new ProgressBar($this->output, count($grav['pages']->routes()) - 1); + $this->progress->setFormat('Scanning %current% pages [%bar%] %percent:3s%% %elapsed:6s%'); + $this->progress->setBarWidth(100); + + $io->title('Grav Security Check'); + $io->newline(2); + + $output = Security::detectXssFromPages($grav['pages'], false, [$this, 'outputProgress']); + + $error = 0; + if (!empty($output)) { + $counter = 1; + foreach ($output as $route => $results) { + $results_parts = array_map(static function ($value, $key) { + return $key.': \''.$value . '\''; + }, array_values($results), array_keys($results)); + + $io->writeln($counter++ .' - ' . $route . '' . implode(', ', $results_parts) . ''); + } + + $error = 1; + $io->error('Security Scan complete: ' . count($output) . ' potential XSS issues found...'); + } else { + $io->success('Security Scan complete: No issues found...'); + } + + $io->newline(1); + + return $error; + } + + /** + * @param array $args + * @return void + */ + public function outputProgress(array $args): void + { + switch ($args['type']) { + case 'count': + $steps = $args['steps']; + $freq = (int)($steps > 100 ? round($steps / 100) : $steps); + $this->progress->setMaxSteps($steps); + $this->progress->setRedrawFrequency($freq); + break; + case 'progress': + if (isset($args['complete']) && $args['complete']) { + $this->progress->finish(); + } else { + $this->progress->advance(); + } + break; + } + } +} diff --git a/system/src/Grav/Console/Cli/ServerCommand.php b/system/src/Grav/Console/Cli/ServerCommand.php new file mode 100644 index 0000000..77bce8b --- /dev/null +++ b/system/src/Grav/Console/Cli/ServerCommand.php @@ -0,0 +1,154 @@ +setName('server') + ->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'Preferred HTTP port rather than auto-find (default is 8000-9000') + ->addOption('symfony', null, InputOption::VALUE_NONE, 'Force using Symfony server') + ->addOption('php', null, InputOption::VALUE_NONE, 'Force using built-in PHP server') + ->setDescription("Runs built-in web-server, Symfony first, then tries PHP's") + ->setHelp("Runs built-in web-server, Symfony first, then tries PHP's"); + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + $io->title('Grav Web Server'); + + // Ensure CLI colors are on + ini_set('cli_server.color', 'on'); + + // Options + $force_symfony = $input->getOption('symfony'); + $force_php = $input->getOption('php'); + + // Find PHP + $executableFinder = new PhpExecutableFinder(); + $php = $executableFinder->find(false); + + $this->ip = '127.0.0.1'; + $this->port = (int)($input->getOption('port') ?? 8000); + + // Get an open port + while (!$this->portAvailable($this->ip, $this->port)) { + $this->port++; + } + + // Setup the commands + $symfony_cmd = ['symfony', 'server:start', '--ansi', '--port=' . $this->port]; + $php_cmd = [$php, '-S', $this->ip.':'.$this->port, 'system/router.php']; + + $commands = [ + self::SYMFONY_SERVER => $symfony_cmd, + self::PHP_SERVER => $php_cmd + ]; + + if ($force_symfony) { + unset($commands[self::PHP_SERVER]); + } elseif ($force_php) { + unset($commands[self::SYMFONY_SERVER]); + } + + $error = 0; + foreach ($commands as $name => $command) { + $process = $this->runProcess($name, $command); + if (!$process) { + $io->note('Starting ' . $name . '...'); + } + + // Should only get here if there's an error running + if (!$process->isRunning() && (($name === self::SYMFONY_SERVER && $force_symfony) || ($name === self::PHP_SERVER))) { + $error = 1; + $io->error('Could not start ' . $name); + } + } + + return $error; + } + + /** + * @param string $name + * @param array $cmd + * @return Process + */ + protected function runProcess(string $name, array $cmd): Process + { + $io = $this->getIO(); + + $process = new Process($cmd); + $process->setTimeout(0); + $process->start(); + + if ($name === self::SYMFONY_SERVER && Utils::contains($process->getErrorOutput(), 'symfony: not found')) { + $io->error('The symfony binary could not be found, please install the CLI tools: https://symfony.com/download'); + $io->warning('Falling back to PHP web server...'); + } + + if ($name === self::PHP_SERVER) { + $io->success('Built-in PHP web server listening on http://' . $this->ip . ':' . $this->port . ' (PHP v' . PHP_VERSION . ')'); + } + + $process->wait(function ($type, $buffer) { + $this->getIO()->write($buffer); + }); + + return $process; + } + + /** + * Simple function test the port + * + * @param string $ip + * @param int $port + * @return bool + */ + protected function portAvailable(string $ip, int $port): bool + { + $fp = @fsockopen($ip, $port, $errno, $errstr, 0.1); + if (!$fp) { + return true; + } + + fclose($fp); + + return false; + } +} diff --git a/system/src/Grav/Console/Cli/YamlLinterCommand.php b/system/src/Grav/Console/Cli/YamlLinterCommand.php new file mode 100644 index 0000000..a9628cd --- /dev/null +++ b/system/src/Grav/Console/Cli/YamlLinterCommand.php @@ -0,0 +1,124 @@ +setName('yamllinter') + ->addOption( + 'all', + 'a', + InputOption::VALUE_NONE, + 'Go through the whole Grav installation' + ) + ->addOption( + 'folder', + 'f', + InputOption::VALUE_OPTIONAL, + 'Go through specific folder' + ) + ->setDescription('Checks various files for YAML errors') + ->setHelp('Checks various files for YAML errors'); + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + $io->title('Yaml Linter'); + + $error = 0; + if ($input->getOption('all')) { + $io->section('All'); + $errors = YamlLinter::lint(''); + + if (empty($errors)) { + $io->success('No YAML Linting issues found'); + } else { + $error = 1; + $this->displayErrors($errors, $io); + } + } elseif ($folder = $input->getOption('folder')) { + $io->section($folder); + $errors = YamlLinter::lint($folder); + + if (empty($errors)) { + $io->success('No YAML Linting issues found'); + } else { + $error = 1; + $this->displayErrors($errors, $io); + } + } else { + $io->section('User Configuration'); + $errors = YamlLinter::lintConfig(); + + if (empty($errors)) { + $io->success('No YAML Linting issues with configuration'); + } else { + $error = 1; + $this->displayErrors($errors, $io); + } + + $io->section('Pages Frontmatter'); + $errors = YamlLinter::lintPages(); + + if (empty($errors)) { + $io->success('No YAML Linting issues with pages'); + } else { + $error = 1; + $this->displayErrors($errors, $io); + } + + $io->section('Page Blueprints'); + $errors = YamlLinter::lintBlueprints(); + + if (empty($errors)) { + $io->success('No YAML Linting issues with blueprints'); + } else { + $error = 1; + $this->displayErrors($errors, $io); + } + } + + return $error; + } + + /** + * @param array $errors + * @param SymfonyStyle $io + * @return void + */ + protected function displayErrors(array $errors, SymfonyStyle $io): void + { + $io->error('YAML Linting issues found...'); + foreach ($errors as $path => $error) { + $io->writeln("{$path} - {$error}"); + } + } +} diff --git a/system/src/Grav/Console/ConsoleCommand.php b/system/src/Grav/Console/ConsoleCommand.php new file mode 100644 index 0000000..044f1b0 --- /dev/null +++ b/system/src/Grav/Console/ConsoleCommand.php @@ -0,0 +1,46 @@ +setupConsole($input, $output); + + return $this->serve(); + } + + /** + * Override with your implementation. + * + * @return int + */ + protected function serve() + { + // Return error. + return 1; + } +} diff --git a/system/src/Grav/Console/ConsoleTrait.php b/system/src/Grav/Console/ConsoleTrait.php new file mode 100644 index 0000000..feb10d3 --- /dev/null +++ b/system/src/Grav/Console/ConsoleTrait.php @@ -0,0 +1,338 @@ +argv = $_SERVER['argv'][0]; + $this->input = $input; + $this->output = new SymfonyStyle($input, $output); + + $this->setupGrav(); + } + + public function getInput(): InputInterface + { + return $this->input; + } + + /** + * @return SymfonyStyle + */ + public function getIO(): SymfonyStyle + { + return $this->output; + } + + /** + * Adds an option. + * + * @param string $name The option name + * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param int|null $mode The option mode: One of the InputOption::VALUE_* constants + * @param string $description A description text + * @param string|string[]|int|bool|null $default The default value (must be null for InputOption::VALUE_NONE) + * @return $this + * @throws InvalidArgumentException If option mode is invalid or incompatible + */ + public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + if ($name !== 'env' && $name !== 'lang') { + parent::addOption($name, $shortcut, $mode, $description, $default); + } + + return $this; + } + + /** + * @return void + */ + final protected function setupGrav(): void + { + try { + $language = $this->input->getOption('lang'); + if ($language) { + // Set used language. + $this->setLanguage($language); + } + } catch (InvalidArgumentException $e) {} + + // Initialize cache with CLI compatibility + $grav = Grav::instance(); + $grav['config']->set('system.cache.cli_compatibility', true); + } + + /** + * Initialize Grav. + * + * - Load configuration + * - Initialize logger + * - Disable debugger + * - Set timezone, locale + * - Load plugins (call PluginsLoadedEvent) + * - Set Pages and Users type to be used in the site + * + * Safe to be called multiple times. + * + * @return $this + */ + final protected function initializeGrav() + { + InitializeProcessor::initializeCli(Grav::instance()); + + return $this; + } + + /** + * Set language to be used in CLI. + * + * @param string|null $code + * @return $this + */ + final protected function setLanguage(string $code = null) + { + $this->initializeGrav(); + + $grav = Grav::instance(); + /** @var Language $language */ + $language = $grav['language']; + if ($language->enabled()) { + if ($code && $language->validate($code)) { + $language->setActive($code); + } else { + $language->setActive($language->getDefault()); + } + } + + return $this; + } + + /** + * Properly initialize plugins. + * + * - call $this->initializeGrav() + * - call onPluginsInitialized event + * + * Safe to be called multiple times. + * + * @return $this + */ + final protected function initializePlugins() + { + if (!$this->plugins_initialized) { + $this->plugins_initialized = true; + + $this->initializeGrav(); + + // Initialize plugins. + $grav = Grav::instance(); + $grav['plugins']->init(); + $grav->fireEvent('onPluginsInitialized'); + } + + return $this; + } + + /** + * Properly initialize themes. + * + * - call $this->initializePlugins() + * - initialize theme (call onThemeInitialized event) + * + * Safe to be called multiple times. + * + * @return $this + */ + final protected function initializeThemes() + { + if (!$this->themes_initialized) { + $this->themes_initialized = true; + + $this->initializePlugins(); + + // Initialize themes. + $grav = Grav::instance(); + $grav['themes']->init(); + } + + return $this; + } + + /** + * Properly initialize pages. + * + * - call $this->initializeThemes() + * - initialize assets (call onAssetsInitialized event) + * - initialize twig (calls the twig events) + * - initialize pages (calls onPagesInitialized event) + * + * Safe to be called multiple times. + * + * @return $this + */ + final protected function initializePages() + { + if (!$this->pages_initialized) { + $this->pages_initialized = true; + + $this->initializeThemes(); + + $grav = Grav::instance(); + + // Initialize assets. + $grav['assets']->init(); + $grav->fireEvent('onAssetsInitialized'); + + // Initialize twig. + $grav['twig']->init(); + + // Initialize pages. + $pages = $grav['pages']; + $pages->init(); + $grav->fireEvent('onPagesInitialized', new Event(['pages' => $pages])); + } + + return $this; + } + + /** + * @param string $path + * @return void + */ + public function isGravInstance($path) + { + $io = $this->getIO(); + + if (!file_exists($path)) { + $io->writeln(''); + $io->writeln("ERROR: Destination doesn't exist:"); + $io->writeln(" $path"); + $io->writeln(''); + exit; + } + + if (!is_dir($path)) { + $io->writeln(''); + $io->writeln("ERROR: Destination chosen to install is not a directory:"); + $io->writeln(" $path"); + $io->writeln(''); + exit; + } + + if (!file_exists($path . DS . 'index.php') || !file_exists($path . DS . '.dependencies') || !file_exists($path . DS . 'system' . DS . 'config' . DS . 'system.yaml')) { + $io->writeln(''); + $io->writeln('ERROR: Destination chosen to install does not appear to be a Grav instance:'); + $io->writeln(" $path"); + $io->writeln(''); + exit; + } + } + + /** + * @param string $path + * @param string $action + * @return string|false + */ + public function composerUpdate($path, $action = 'install') + { + $composer = Composer::getComposerExecutor(); + + return system($composer . ' --working-dir="'.$path.'" --no-interaction --no-dev --prefer-dist -o '. $action); + } + + /** + * @param array $all + * @return int + * @throws Exception + */ + public function clearCache($all = []) + { + if ($all) { + $all = ['--all' => true]; + } + + $command = new ClearCacheCommand(); + $input = new ArrayInput($all); + return $command->run($input, $this->output); + } + + /** + * @return void + */ + public function invalidateCache() + { + Cache::invalidateCache(); + } + + /** + * Load the local config file + * + * @return string|false The local config file name. false if local config does not exist + */ + public function loadLocalConfig() + { + $home_folder = getenv('HOME') ?: getenv('HOMEDRIVE') . getenv('HOMEPATH'); + $local_config_file = $home_folder . '/.grav/config'; + + if (file_exists($local_config_file)) { + $file = YamlFile::instance($local_config_file); + $this->local_config = $file->content(); + $file->free(); + + return $local_config_file; + } + + return false; + } +} diff --git a/system/src/Grav/Console/Gpm/DirectInstallCommand.php b/system/src/Grav/Console/Gpm/DirectInstallCommand.php new file mode 100644 index 0000000..e5c662b --- /dev/null +++ b/system/src/Grav/Console/Gpm/DirectInstallCommand.php @@ -0,0 +1,322 @@ +setName('direct-install') + ->setAliases(['directinstall']) + ->addArgument( + 'package-file', + InputArgument::REQUIRED, + 'Installable package local or remote . Can install specific version' + ) + ->addOption( + 'all-yes', + 'y', + InputOption::VALUE_NONE, + 'Assumes yes (or best approach) instead of prompting' + ) + ->addOption( + 'destination', + 'd', + InputOption::VALUE_OPTIONAL, + 'The destination where the package should be installed at. By default this would be where the grav instance has been launched from', + GRAV_ROOT + ) + ->setDescription('Installs Grav, plugin, or theme directly from a file or a URL') + ->setHelp('The direct-install command installs Grav, plugin, or theme directly from a file or a URL'); + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + if (!class_exists(ZipArchive::class)) { + $io->title('Direct Install'); + $io->error('php-zip extension needs to be enabled!'); + + return 1; + } + + // Making sure the destination is usable + $this->destination = realpath($input->getOption('destination')); + + if (!Installer::isGravInstance($this->destination) || + !Installer::isValidDestination($this->destination, [Installer::EXISTS, Installer::IS_LINK]) + ) { + $io->writeln('ERROR: ' . Installer::lastErrorMsg()); + + return 1; + } + + $this->all_yes = $input->getOption('all-yes'); + + $package_file = $input->getArgument('package-file'); + + $question = new ConfirmationQuestion("Are you sure you want to direct-install {$package_file} [y|N] ", false); + + $answer = $this->all_yes ? true : $io->askQuestion($question); + + if (!$answer) { + $io->writeln('exiting...'); + $io->newLine(); + + return 1; + } + + $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true); + $tmp_zip = $tmp_dir . uniqid('/Grav-', false); + + $io->newLine(); + $io->writeln("Preparing to install {$package_file}"); + + $zip = null; + if (Response::isRemote($package_file)) { + $io->write(' |- Downloading package... 0%'); + try { + $zip = GPM::downloadPackage($package_file, $tmp_zip); + } catch (RuntimeException $e) { + $io->newLine(); + $io->writeln(" `- ERROR: {$e->getMessage()}"); + $io->newLine(); + + return 1; + } + + if ($zip) { + $io->write("\x0D"); + $io->write(' |- Downloading package... 100%'); + $io->newLine(); + } + } elseif (is_file($package_file)) { + $io->write(' |- Copying package... 0%'); + $zip = GPM::copyPackage($package_file, $tmp_zip); + if ($zip) { + $io->write("\x0D"); + $io->write(' |- Copying package... 100%'); + $io->newLine(); + } + } + + if ($zip && file_exists($zip)) { + $tmp_source = $tmp_dir . uniqid('/Grav-', false); + + $io->write(' |- Extracting package... '); + $extracted = Installer::unZip($zip, $tmp_source); + + if (!$extracted) { + $io->write("\x0D"); + $io->writeln(' |- Extracting package... failed'); + Folder::delete($tmp_source); + Folder::delete($tmp_zip); + + return 1; + } + + $io->write("\x0D"); + $io->writeln(' |- Extracting package... ok'); + + + $type = GPM::getPackageType($extracted); + + if (!$type) { + $io->writeln(" '- ERROR: Not a valid Grav package"); + $io->newLine(); + Folder::delete($tmp_source); + Folder::delete($tmp_zip); + + return 1; + } + + $blueprint = GPM::getBlueprints($extracted); + if ($blueprint) { + if (isset($blueprint['dependencies'])) { + $dependencies = []; + foreach ($blueprint['dependencies'] as $dependency) { + if (is_array($dependency)) { + if (isset($dependency['name'])) { + $dependencies[] = $dependency['name']; + } + if (isset($dependency['github'])) { + $dependencies[] = $dependency['github']; + } + } else { + $dependencies[] = $dependency; + } + } + $io->writeln(' |- Dependencies found... [' . implode(',', $dependencies) . ']'); + + $question = new ConfirmationQuestion(" | '- Dependencies will not be satisfied. Continue ? [y|N] ", false); + $answer = $this->all_yes ? true : $io->askQuestion($question); + + if (!$answer) { + $io->writeln('exiting...'); + $io->newLine(); + Folder::delete($tmp_source); + Folder::delete($tmp_zip); + + return 1; + } + } + } + + if ($type === 'grav') { + $io->write(' |- Checking destination... '); + Installer::isValidDestination(GRAV_ROOT . '/system'); + if (Installer::IS_LINK === Installer::lastErrorCode()) { + $io->write("\x0D"); + $io->writeln(' |- Checking destination... symbolic link'); + $io->writeln(" '- ERROR: symlinks found... " . GRAV_ROOT . ''); + $io->newLine(); + Folder::delete($tmp_source); + Folder::delete($tmp_zip); + + return 1; + } + + $io->write("\x0D"); + $io->writeln(' |- Checking destination... ok'); + + $io->write(' |- Installing package... '); + + $this->upgradeGrav($zip, $extracted); + } else { + $name = GPM::getPackageName($extracted); + + if (!$name) { + $io->writeln('ERROR: Name could not be determined. Please specify with --name|-n'); + $io->newLine(); + Folder::delete($tmp_source); + Folder::delete($tmp_zip); + + return 1; + } + + $install_path = GPM::getInstallPath($type, $name); + $is_update = file_exists($install_path); + + $io->write(' |- Checking destination... '); + + Installer::isValidDestination(GRAV_ROOT . DS . $install_path); + if (Installer::lastErrorCode() === Installer::IS_LINK) { + $io->write("\x0D"); + $io->writeln(' |- Checking destination... symbolic link'); + $io->writeln(" '- ERROR: symlink found... " . GRAV_ROOT . DS . $install_path . ''); + $io->newLine(); + Folder::delete($tmp_source); + Folder::delete($tmp_zip); + + return 1; + } + + $io->write("\x0D"); + $io->writeln(' |- Checking destination... ok'); + + $io->write(' |- Installing package... '); + + Installer::install( + $zip, + $this->destination, + $options = [ + 'install_path' => $install_path, + 'theme' => (($type === 'theme')), + 'is_update' => $is_update + ], + $extracted + ); + + // clear cache after successful upgrade + $this->clearCache(); + } + + Folder::delete($tmp_source); + + $io->write("\x0D"); + + if (Installer::lastErrorCode()) { + $io->writeln(" '- " . Installer::lastErrorMsg() . ''); + $io->newLine(); + } else { + $io->writeln(' |- Installing package... ok'); + $io->writeln(" '- Success! "); + $io->newLine(); + } + } else { + $io->writeln(" '- ERROR: ZIP package could not be found"); + Folder::delete($tmp_zip); + + return 1; + } + + Folder::delete($tmp_zip); + + return 0; + } + + /** + * @param string $zip + * @param string $folder + * @return void + */ + private function upgradeGrav(string $zip, string $folder): void + { + if (!is_dir($folder)) { + Installer::setError('Invalid source folder'); + } + + try { + $script = $folder . '/system/install.php'; + /** Install $installer */ + if ((file_exists($script) && $install = include $script) && is_callable($install)) { + $install($zip); + } else { + throw new RuntimeException('Uploaded archive file is not a valid Grav update package'); + } + } catch (Exception $e) { + Installer::setError($e->getMessage()); + } + } +} diff --git a/system/src/Grav/Console/Gpm/IndexCommand.php b/system/src/Grav/Console/Gpm/IndexCommand.php new file mode 100644 index 0000000..ecc91fe --- /dev/null +++ b/system/src/Grav/Console/Gpm/IndexCommand.php @@ -0,0 +1,335 @@ +setName('index') + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force re-fetching the data from remote' + ) + ->addOption( + 'filter', + 'F', + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Allows to limit the results based on one or multiple filters input. This can be either portion of a name/slug or a regex' + ) + ->addOption( + 'themes-only', + 'T', + InputOption::VALUE_NONE, + 'Filters the results to only Themes' + ) + ->addOption( + 'plugins-only', + 'P', + InputOption::VALUE_NONE, + 'Filters the results to only Plugins' + ) + ->addOption( + 'updates-only', + 'U', + InputOption::VALUE_NONE, + 'Filters the results to Updatable Themes and Plugins only' + ) + ->addOption( + 'installed-only', + 'I', + InputOption::VALUE_NONE, + 'Filters the results to only the Themes and Plugins you have installed' + ) + ->addOption( + 'sort', + 's', + InputOption::VALUE_REQUIRED, + 'Allows to sort (ASC) the results. SORT can be either "name", "slug", "author", "date"', + 'date' + ) + ->addOption( + 'desc', + 'D', + InputOption::VALUE_NONE, + 'Reverses the order of the output.' + ) + ->addOption( + 'enabled', + 'e', + InputOption::VALUE_NONE, + 'Filters the results to only enabled Themes and Plugins.' + ) + ->addOption( + 'disabled', + 'd', + InputOption::VALUE_NONE, + 'Filters the results to only disabled Themes and Plugins.' + ) + ->setDescription('Lists the plugins and themes available for installation') + ->setHelp('The index command lists the plugins and themes available for installation') + ; + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $this->options = $input->getOptions(); + $this->gpm = new GPM($this->options['force']); + $this->displayGPMRelease(); + $this->data = $this->gpm->getRepository(); + + $data = $this->filter($this->data); + + $io = $this->getIO(); + + if (count($data) === 0) { + $io->writeln('No data was found in the GPM repository stored locally.'); + $io->writeln('Please try clearing cache and running the bin/gpm index -f command again'); + $io->writeln('If this doesn\'t work try tweaking your GPM system settings.'); + $io->newLine(); + $io->writeln('For more help go to:'); + $io->writeln(' -> https://learn.getgrav.org/troubleshooting/common-problems#cannot-connect-to-the-gpm'); + + return 1; + } + + foreach ($data as $type => $packages) { + $io->writeln('' . strtoupper($type) . ' [ ' . count($packages) . ' ]'); + + $packages = $this->sort($packages); + + if (!empty($packages)) { + $io->section('Packages table'); + $table = new Table($io); + $table->setHeaders(['Count', 'Name', 'Slug', 'Version', 'Installed', 'Enabled']); + + $index = 0; + foreach ($packages as $slug => $package) { + $row = [ + 'Count' => $index++ + 1, + 'Name' => '' . Utils::truncate($package->name, 20, false, ' ', '...') . ' ', + 'Slug' => $slug, + 'Version'=> $this->version($package), + 'Installed' => $this->installed($package), + 'Enabled' => $this->enabled($package), + ]; + + $table->addRow($row); + } + + $table->render(); + } + + $io->newLine(); + } + + $io->writeln('You can either get more informations about a package by typing:'); + $io->writeln(" {$this->argv} info "); + $io->newLine(); + $io->writeln('Or you can install a package by typing:'); + $io->writeln(" {$this->argv} install "); + $io->newLine(); + + return 0; + } + + /** + * @param Package $package + * @return string + */ + private function version(Package $package): string + { + $list = $this->gpm->{'getUpdatable' . ucfirst($package->package_type)}(); + $package = $list[$package->slug] ?? $package; + $type = ucfirst(preg_replace('/s$/', '', $package->package_type)); + $updatable = $this->gpm->{'is' . $type . 'Updatable'}($package->slug); + $installed = $this->gpm->{'is' . $type . 'Installed'}($package->slug); + $local = $this->gpm->{'getInstalled' . $type}($package->slug); + + if (!$installed || !$updatable) { + $version = $installed ? $local->version : $package->version; + return "v{$version}"; + } + + return "v{$package->version} -> v{$package->available}"; + } + + /** + * @param Package $package + * @return string + */ + private function installed(Package $package): string + { + $type = ucfirst(preg_replace('/s$/', '', $package->package_type)); + $method = 'is' . $type . 'Installed'; + $installed = $this->gpm->{$method}($package->slug); + + return !$installed ? 'not installed' : 'installed'; + } + + /** + * @param Package $package + * @return string + */ + private function enabled(Package $package): string + { + $type = ucfirst(preg_replace('/s$/', '', $package->package_type)); + $method = 'is' . $type . 'Installed'; + $installed = $this->gpm->{$method}($package->slug); + + $result = ''; + if ($installed) { + $method = 'is' . $type . 'Enabled'; + $enabled = $this->gpm->{$method}($package->slug); + if ($enabled === true) { + $result = 'enabled'; + } elseif ($enabled === false) { + $result = 'disabled'; + } + } + + return $result; + } + + /** + * @param Packages $data + * @return Packages + */ + public function filter(Packages $data): Packages + { + // filtering and sorting + if ($this->options['plugins-only']) { + unset($data['themes']); + } + if ($this->options['themes-only']) { + unset($data['plugins']); + } + + $filter = [ + $this->options['desc'], + $this->options['disabled'], + $this->options['enabled'], + $this->options['filter'], + $this->options['installed-only'], + $this->options['updates-only'], + ]; + + if (count(array_filter($filter))) { + foreach ($data as $type => $packages) { + foreach ($packages as $slug => $package) { + $filter = true; + + // Filtering by string + if ($this->options['filter']) { + $filter = preg_grep('/(' . implode('|', $this->options['filter']) . ')/i', [$slug, $package->name]); + } + + // Filtering updatables only + if ($filter && ($this->options['installed-only'] || $this->options['enabled'] || $this->options['disabled'])) { + $method = ucfirst(preg_replace('/s$/', '', $package->package_type)); + $function = 'is' . $method . 'Installed'; + $filter = $this->gpm->{$function}($package->slug); + } + + // Filtering updatables only + if ($filter && $this->options['updates-only']) { + $method = ucfirst(preg_replace('/s$/', '', $package->package_type)); + $function = 'is' . $method . 'Updatable'; + $filter = $this->gpm->{$function}($package->slug); + } + + // Filtering enabled only + if ($filter && $this->options['enabled']) { + $method = ucfirst(preg_replace('/s$/', '', $package->package_type)); + + // Check if packaged is enabled. + $function = 'is' . $method . 'Enabled'; + $filter = $this->gpm->{$function}($package->slug); + } + + // Filtering disabled only + if ($filter && $this->options['disabled']) { + $method = ucfirst(preg_replace('/s$/', '', $package->package_type)); + + // Check if package is disabled. + $function = 'is' . $method . 'Enabled'; + $enabled_filter = $this->gpm->{$function}($package->slug); + + // Apply filtering results. + if (!( $enabled_filter === false)) { + $filter = false; + } + } + + if (!$filter) { + unset($data[$type][$slug]); + } + } + } + } + + return $data; + } + + /** + * @param AbstractPackageCollection|Plugins|Themes $packages + * @return array + */ + public function sort(AbstractPackageCollection $packages): array + { + $key = $this->options['sort']; + + // Sorting only works once. + return $packages->sort( + function ($a, $b) use ($key) { + switch ($key) { + case 'author': + return strcmp($a->{$key}['name'], $b->{$key}['name']); + default: + return strcmp($a->$key, $b->$key); + } + }, + $this->options['desc'] ? true : false + ); + } +} diff --git a/system/src/Grav/Console/Gpm/InfoCommand.php b/system/src/Grav/Console/Gpm/InfoCommand.php new file mode 100644 index 0000000..5dddd32 --- /dev/null +++ b/system/src/Grav/Console/Gpm/InfoCommand.php @@ -0,0 +1,191 @@ +setName('info') + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force fetching the new data remotely' + ) + ->addOption( + 'all-yes', + 'y', + InputOption::VALUE_NONE, + 'Assumes yes (or best approach) instead of prompting' + ) + ->addArgument( + 'package', + InputArgument::REQUIRED, + 'The package of which more informations are desired. Use the "index" command for a list of packages' + ) + ->setDescription('Shows more informations about a package') + ->setHelp('The info shows more information about a package'); + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + $this->gpm = new GPM($input->getOption('force')); + + $this->all_yes = $input->getOption('all-yes'); + + $this->displayGPMRelease(); + + $foundPackage = $this->gpm->findPackage($input->getArgument('package')); + + if (!$foundPackage) { + $io->writeln("The package '{$input->getArgument('package')}' was not found in the Grav repository."); + $io->newLine(); + $io->writeln('You can list all the available packages by typing:'); + $io->writeln(" {$this->argv} index"); + $io->newLine(); + + return 1; + } + + $io->writeln("Found package '{$input->getArgument('package')}' under the '" . ucfirst($foundPackage->package_type) . "' section"); + $io->newLine(); + $io->writeln("{$foundPackage->name} [{$foundPackage->slug}]"); + $io->writeln(str_repeat('-', strlen($foundPackage->name) + strlen($foundPackage->slug) + 3)); + $io->writeln('' . strip_tags($foundPackage->description_plain) . ''); + $io->newLine(); + + $packageURL = ''; + if (isset($foundPackage->author['url'])) { + $packageURL = '<' . $foundPackage->author['url'] . '>'; + } + + $io->writeln('' . str_pad( + 'Author', + 12 + ) . ': ' . $foundPackage->author['name'] . ' <' . $foundPackage->author['email'] . '> ' . $packageURL); + + foreach ([ + 'version', + 'keywords', + 'date', + 'homepage', + 'demo', + 'docs', + 'guide', + 'repository', + 'bugs', + 'zipball_url', + 'license' + ] as $info) { + if (isset($foundPackage->{$info})) { + $name = ucfirst($info); + $data = $foundPackage->{$info}; + + if ($info === 'zipball_url') { + $name = 'Download'; + } + + if ($info === 'date') { + $name = 'Last Update'; + $data = date('D, j M Y, H:i:s, P ', strtotime($data)); + } + + $name = str_pad($name, 12); + $io->writeln("{$name}: {$data}"); + } + } + + $type = rtrim($foundPackage->package_type, 's'); + $updatable = $this->gpm->{'is' . $type . 'Updatable'}($foundPackage->slug); + $installed = $this->gpm->{'is' . $type . 'Installed'}($foundPackage->slug); + + // display current version if installed and different + if ($installed && $updatable) { + $local = $this->gpm->{'getInstalled'. $type}($foundPackage->slug); + $io->newLine(); + $io->writeln("Currently installed version: {$local->version}"); + $io->newLine(); + } + + // display changelog information + $question = new ConfirmationQuestion( + 'Would you like to read the changelog? [y|N] ', + false + ); + $answer = $this->all_yes ? true : $io->askQuestion($question); + + if ($answer) { + $changelog = $foundPackage->changelog; + + $io->newLine(); + foreach ($changelog as $version => $log) { + $title = $version . ' [' . $log['date'] . ']'; + $content = preg_replace_callback('/\d\.\s\[\]\(#(.*)\)/', static function ($match) { + return "\n" . ucfirst($match[1]) . ':'; + }, $log['content']); + + $io->writeln("{$title}"); + $io->writeln(str_repeat('-', strlen($title))); + $io->writeln($content); + $io->newLine(); + + $question = new ConfirmationQuestion('Press [ENTER] to continue or [q] to quit ', true); + $answer = $this->all_yes ? false : $io->askQuestion($question); + if (!$answer) { + break; + } + $io->newLine(); + } + } + + $io->newLine(); + + if ($installed && $updatable) { + $io->writeln('You can update this package by typing:'); + $io->writeln(" {$this->argv} update {$foundPackage->slug}"); + } else { + $io->writeln('You can install this package by typing:'); + $io->writeln(" {$this->argv} install {$foundPackage->slug}"); + } + + $io->newLine(); + + return 0; + } +} diff --git a/system/src/Grav/Console/Gpm/InstallCommand.php b/system/src/Grav/Console/Gpm/InstallCommand.php new file mode 100644 index 0000000..92c8e07 --- /dev/null +++ b/system/src/Grav/Console/Gpm/InstallCommand.php @@ -0,0 +1,726 @@ +setName('install') + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force re-fetching the data from remote' + ) + ->addOption( + 'all-yes', + 'y', + InputOption::VALUE_NONE, + 'Assumes yes (or best approach) instead of prompting' + ) + ->addOption( + 'destination', + 'd', + InputOption::VALUE_OPTIONAL, + 'The destination where the package should be installed at. By default this would be where the grav instance has been launched from', + GRAV_ROOT + ) + ->addArgument( + 'package', + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + 'Package(s) to install. Use "bin/gpm index" to list packages. Use "bin/gpm direct-install" to install a specific version' + ) + ->setDescription('Performs the installation of plugins and themes') + ->setHelp('The install command allows to install plugins and themes'); + } + + /** + * Allows to set the GPM object, used for testing the class + * + * @param GPM $gpm + */ + public function setGpm(GPM $gpm): void + { + $this->gpm = $gpm; + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + if (!class_exists(ZipArchive::class)) { + $io->title('GPM Install'); + $io->error('php-zip extension needs to be enabled!'); + + return 1; + } + + $this->gpm = new GPM($input->getOption('force')); + + $this->all_yes = $input->getOption('all-yes'); + + $this->displayGPMRelease(); + + $this->destination = realpath($input->getOption('destination')); + + $packages = array_map('strtolower', $input->getArgument('package')); + $this->data = $this->gpm->findPackages($packages); + $this->loadLocalConfig(); + + if (!Installer::isGravInstance($this->destination) || + !Installer::isValidDestination($this->destination, [Installer::EXISTS, Installer::IS_LINK]) + ) { + $io->writeln('ERROR: ' . Installer::lastErrorMsg()); + + return 1; + } + + $io->newLine(); + + if (!$this->data['total']) { + $io->writeln('Nothing to install.'); + $io->newLine(); + + return 0; + } + + if (count($this->data['not_found'])) { + $io->writeln('These packages were not found on Grav: ' . implode( + ', ', + array_keys($this->data['not_found']) + ) . ''); + } + + unset($this->data['not_found'], $this->data['total']); + + if (null !== $this->local_config) { + // Symlinks available, ask if Grav should use them + $this->use_symlinks = false; + $question = new ConfirmationQuestion('Should Grav use the symlinks if available? [y|N] ', false); + + $answer = $this->all_yes ? false : $io->askQuestion($question); + + if ($answer) { + $this->use_symlinks = true; + } + } + + $io->newLine(); + + try { + $dependencies = $this->gpm->getDependencies($packages); + } catch (Exception $e) { + //Error out if there are incompatible packages requirements and tell which ones, and what to do + //Error out if there is any error in parsing the dependencies and their versions, and tell which one is broken + $io->writeln("{$e->getMessage()}"); + + return 1; + } + + if ($dependencies) { + try { + $this->installDependencies($dependencies, 'install', 'The following dependencies need to be installed...'); + $this->installDependencies($dependencies, 'update', 'The following dependencies need to be updated...'); + $this->installDependencies($dependencies, 'ignore', "The following dependencies can be updated as there is a newer version, but it's not mandatory...", false); + } catch (Exception $e) { + $io->writeln('Installation aborted'); + + return 1; + } + + $io->writeln('Dependencies are OK'); + $io->newLine(); + } + + + //We're done installing dependencies. Install the actual packages + foreach ($this->data as $data) { + foreach ($data as $package_name => $package) { + if (array_key_exists($package_name, $dependencies)) { + $io->writeln("Package {$package_name} already installed as dependency"); + } else { + $is_valid_destination = Installer::isValidDestination($this->destination . DS . $package->install_path); + if ($is_valid_destination || Installer::lastErrorCode() == Installer::NOT_FOUND) { + $this->processPackage($package, false); + } else { + if (Installer::lastErrorCode() == Installer::EXISTS) { + try { + $this->askConfirmationIfMajorVersionUpdated($package); + $this->gpm->checkNoOtherPackageNeedsThisDependencyInALowerVersion($package->slug, $package->available, array_keys($data)); + } catch (Exception $e) { + $io->writeln("{$e->getMessage()}"); + + return 1; + } + + $question = new ConfirmationQuestion("The package {$package_name} is already installed, overwrite? [y|N] ", false); + $answer = $this->all_yes ? true : $io->askQuestion($question); + + if ($answer) { + $is_update = true; + $this->processPackage($package, $is_update); + } else { + $io->writeln("Package {$package_name} not overwritten"); + } + } else { + if (Installer::lastErrorCode() == Installer::IS_LINK) { + $io->writeln("Cannot overwrite existing symlink for {$package_name}"); + $io->newLine(); + } + } + } + } + } + } + + if (count($this->demo_processing) > 0) { + foreach ($this->demo_processing as $package) { + $this->installDemoContent($package); + } + } + + // clear cache after successful upgrade + $this->clearCache(); + + return 0; + } + + /** + * If the package is updated from an older major release, show warning and ask confirmation + * + * @param Package $package + * @return void + */ + public function askConfirmationIfMajorVersionUpdated(Package $package): void + { + $io = $this->getIO(); + $package_name = $package->name; + $new_version = $package->available ?: $this->gpm->getLatestVersionOfPackage($package->slug); + $old_version = $package->version; + + $major_version_changed = explode('.', $new_version)[0] !== explode('.', $old_version)[0]; + + if ($major_version_changed) { + if ($this->all_yes) { + $io->writeln("The package {$package_name} will be updated to a new major version {$new_version}, from {$old_version}"); + return; + } + + $question = new ConfirmationQuestion("The package {$package_name} will be updated to a new major version {$new_version}, from {$old_version}. Be sure to read what changed with the new major release. Continue? [y|N] ", false); + + if (!$io->askQuestion($question)) { + $io->writeln("Package {$package_name} not updated"); + exit; + } + } + } + + /** + * Given a $dependencies list, filters their type according to $type and + * shows $message prior to listing them to the user. Then asks the user a confirmation prior + * to installing them. + * + * @param array $dependencies The dependencies array + * @param string $type The type of dependency to show: install, update, ignore + * @param string $message A message to be shown prior to listing the dependencies + * @param bool $required A flag that determines if the installation is required or optional + * @return void + * @throws Exception + */ + public function installDependencies(array $dependencies, string $type, string $message, bool $required = true): void + { + $io = $this->getIO(); + $packages = array_filter($dependencies, static function ($action) use ($type) { + return $action === $type; + }); + if (count($packages) > 0) { + $io->writeln($message); + + foreach ($packages as $dependencyName => $dependencyVersion) { + $io->writeln(" |- Package {$dependencyName}"); + } + + $io->newLine(); + + if ($type === 'install') { + $questionAction = 'Install'; + } else { + $questionAction = 'Update'; + } + + if (count($packages) === 1) { + $questionArticle = 'this'; + } else { + $questionArticle = 'these'; + } + + if (count($packages) === 1) { + $questionNoun = 'package'; + } else { + $questionNoun = 'packages'; + } + + $question = new ConfirmationQuestion("${questionAction} {$questionArticle} {$questionNoun}? [Y|n] ", true); + $answer = $this->all_yes ? true : $io->askQuestion($question); + + if ($answer) { + foreach ($packages as $dependencyName => $dependencyVersion) { + $package = $this->gpm->findPackage($dependencyName); + $this->processPackage($package, $type === 'update'); + } + $io->newLine(); + } elseif ($required) { + throw new Exception(); + } + } + } + + /** + * @param Package|null $package + * @param bool $is_update True if the package is an update + * @return void + */ + private function processPackage(?Package $package, bool $is_update = false): void + { + $io = $this->getIO(); + + if (!$package) { + $io->writeln('Package not found on the GPM!'); + $io->newLine(); + return; + } + + $symlink = false; + if ($this->use_symlinks) { + if (!isset($package->version) || $this->getSymlinkSource($package)) { + $symlink = true; + } + } + + $symlink ? $this->processSymlink($package) : $this->processGpm($package, $is_update); + + $this->processDemo($package); + } + + /** + * Add package to the queue to process the demo content, if demo content exists + * + * @param Package $package + * @return void + */ + private function processDemo(Package $package): void + { + $demo_dir = $this->destination . DS . $package->install_path . DS . '_demo'; + if (file_exists($demo_dir)) { + $this->demo_processing[] = $package; + } + } + + /** + * Prompt to install the demo content of a package + * + * @param Package $package + * @return void + */ + private function installDemoContent(Package $package): void + { + $io = $this->getIO(); + $demo_dir = $this->destination . DS . $package->install_path . DS . '_demo'; + + if (file_exists($demo_dir)) { + $dest_dir = $this->destination . DS . 'user'; + $pages_dir = $dest_dir . DS . 'pages'; + + // Demo content exists, prompt to install it. + $io->writeln("Attention: {$package->name} contains demo content"); + + $question = new ConfirmationQuestion('Do you wish to install this demo content? [y|N] ', false); + + $answer = $io->askQuestion($question); + + if (!$answer) { + $io->writeln(" '- Skipped! "); + $io->newLine(); + + return; + } + + // if pages folder exists in demo + if (file_exists($demo_dir . DS . 'pages')) { + $pages_backup = 'pages.' . date('m-d-Y-H-i-s'); + $question = new ConfirmationQuestion('This will backup your current `user/pages` folder to `user/' . $pages_backup . '`, continue? [y|N]', false); + $answer = $this->all_yes ? true : $io->askQuestion($question); + + if (!$answer) { + $io->writeln(" '- Skipped! "); + $io->newLine(); + + return; + } + + // backup current pages folder + if (file_exists($dest_dir)) { + if (rename($pages_dir, $dest_dir . DS . $pages_backup)) { + $io->writeln(' |- Backing up pages... ok'); + } else { + $io->writeln(' |- Backing up pages... failed'); + } + } + } + + // Confirmation received, copy over the data + $io->writeln(' |- Installing demo content... ok '); + Folder::rcopy($demo_dir, $dest_dir); + $io->writeln(" '- Success! "); + $io->newLine(); + } + } + + /** + * @param Package $package + * @return array|false + */ + private function getGitRegexMatches(Package $package) + { + if (isset($package->repository)) { + $repository = $package->repository; + } else { + return false; + } + + preg_match(GIT_REGEX, $repository, $matches); + + return $matches; + } + + /** + * @param Package $package + * @return string|false + */ + private function getSymlinkSource(Package $package) + { + $matches = $this->getGitRegexMatches($package); + + foreach ($this->local_config as $paths) { + if (Utils::endsWith($matches[2], '.git')) { + $repo_dir = preg_replace('/\.git$/', '', $matches[2]); + } else { + $repo_dir = $matches[2]; + } + + $paths = (array) $paths; + foreach ($paths as $repo) { + $path = rtrim($repo, '/') . '/' . $repo_dir; + if (file_exists($path)) { + return $path; + } + } + } + + return false; + } + + /** + * @param Package $package + * @return void + */ + private function processSymlink(Package $package): void + { + $io = $this->getIO(); + + exec('cd ' . $this->destination); + + $to = $this->destination . DS . $package->install_path; + $from = $this->getSymlinkSource($package); + + $io->writeln("Preparing to Symlink {$package->name}"); + $io->write(' |- Checking source... '); + + if (file_exists($from)) { + $io->writeln('ok'); + + $io->write(' |- Checking destination... '); + $checks = $this->checkDestination($package); + + if (!$checks) { + $io->writeln(" '- Installation failed or aborted."); + $io->newLine(); + } elseif (file_exists($to)) { + $io->writeln(" '- Symlink cannot overwrite an existing package, please remove first"); + $io->newLine(); + } else { + symlink($from, $to); + + // extra white spaces to clear out the buffer properly + $io->writeln(' |- Symlinking package... ok '); + $io->writeln(" '- Success! "); + $io->newLine(); + } + + return; + } + + $io->writeln('not found!'); + $io->writeln(" '- Installation failed or aborted."); + } + + /** + * @param Package $package + * @param bool $is_update + * @return bool + */ + private function processGpm(Package $package, bool $is_update = false) + { + $io = $this->getIO(); + + $version = $package->available ?? $package->version; + $license = Licenses::get($package->slug); + + $io->writeln("Preparing to install {$package->name} [v{$version}]"); + + $io->write(' |- Downloading package... 0%'); + $this->file = $this->downloadPackage($package, $license); + + if (!$this->file) { + $io->writeln(" '- Installation failed or aborted."); + $io->newLine(); + + return false; + } + + $io->write(' |- Checking destination... '); + $checks = $this->checkDestination($package); + + if (!$checks) { + $io->writeln(" '- Installation failed or aborted."); + $io->newLine(); + } else { + $io->write(' |- Installing package... '); + $installation = $this->installPackage($package, $is_update); + if (!$installation) { + $io->writeln(" '- Installation failed or aborted."); + $io->newLine(); + } else { + $io->writeln(" '- Success! "); + $io->newLine(); + + return true; + } + } + + return false; + } + + /** + * @param Package $package + * @param string|null $license + * @return string|null + */ + private function downloadPackage(Package $package, string $license = null) + { + $io = $this->getIO(); + + $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true); + $this->tmp = $tmp_dir . '/Grav-' . uniqid(); + $filename = $package->slug . basename($package->zipball_url); + $filename = preg_replace('/[\\\\\/:"*?&<>|]+/m', '-', $filename); + $query = ''; + + if (!empty($package->premium)) { + $query = json_encode(array_merge( + $package->premium, + [ + 'slug' => $package->slug, + 'filename' => $package->premium['filename'], + 'license_key' => $license, + 'sid' => md5(GRAV_ROOT) + ] + )); + + $query = '?d=' . base64_encode($query); + } + + try { + $output = Response::get($package->zipball_url . $query, [], [$this, 'progress']); + } catch (Exception $e) { + if (!empty($package->premium) && $e->getCode() === 401) { + $message = 'Unauthorized Premium License Key'; + } else { + $message = $e->getMessage(); + } + + $error = str_replace("\n", "\n | '- ", $message); + $io->write("\x0D"); + // extra white spaces to clear out the buffer properly + $io->writeln(' |- Downloading package... error '); + $io->writeln(" | '- " . $error); + + return null; + } + + Folder::create($this->tmp); + + $io->write("\x0D"); + $io->write(' |- Downloading package... 100%'); + $io->newLine(); + + file_put_contents($this->tmp . DS . $filename, $output); + + return $this->tmp . DS . $filename; + } + + /** + * @param Package $package + * @return bool + */ + private function checkDestination(Package $package): bool + { + $io = $this->getIO(); + + Installer::isValidDestination($this->destination . DS . $package->install_path); + + if (Installer::lastErrorCode() === Installer::IS_LINK) { + $io->write("\x0D"); + $io->writeln(' |- Checking destination... symbolic link'); + + if ($this->all_yes) { + $io->writeln(" | '- Skipped automatically."); + + return false; + } + + $question = new ConfirmationQuestion( + " | '- Destination has been detected as symlink, delete symbolic link first? [y|N] ", + false + ); + $answer = $io->askQuestion($question); + + if (!$answer) { + $io->writeln(" | '- You decided to not delete the symlink automatically."); + + return false; + } + + unlink($this->destination . DS . $package->install_path); + } + + $io->write("\x0D"); + $io->writeln(' |- Checking destination... ok'); + + return true; + } + + /** + * Install a package + * + * @param Package $package + * @param bool $is_update True if it's an update. False if it's an install + * @return bool + */ + private function installPackage(Package $package, bool $is_update = false): bool + { + $io = $this->getIO(); + + $type = $package->package_type; + + Installer::install($this->file, $this->destination, ['install_path' => $package->install_path, 'theme' => $type === 'themes', 'is_update' => $is_update]); + $error_code = Installer::lastErrorCode(); + Folder::delete($this->tmp); + + if ($error_code) { + $io->write("\x0D"); + // extra white spaces to clear out the buffer properly + $io->writeln(' |- Installing package... error '); + $io->writeln(" | '- " . Installer::lastErrorMsg()); + + return false; + } + + $message = Installer::getMessage(); + if ($message) { + $io->write("\x0D"); + // extra white spaces to clear out the buffer properly + $io->writeln(" |- {$message}"); + } + + $io->write("\x0D"); + // extra white spaces to clear out the buffer properly + $io->writeln(' |- Installing package... ok '); + + return true; + } + + /** + * @param array $progress + * @return void + */ + public function progress(array $progress): void + { + $io = $this->getIO(); + + $io->write("\x0D"); + $io->write(' |- Downloading package... ' . str_pad( + $progress['percent'], + 5, + ' ', + STR_PAD_LEFT + ) . '%'); + } +} diff --git a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php new file mode 100644 index 0000000..b840016 --- /dev/null +++ b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php @@ -0,0 +1,344 @@ +setName('self-upgrade') + ->setAliases(['selfupgrade', 'selfupdate']) + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force re-fetching the data from remote' + ) + ->addOption( + 'all-yes', + 'y', + InputOption::VALUE_NONE, + 'Assumes yes (or best approach) instead of prompting' + ) + ->addOption( + 'overwrite', + 'o', + InputOption::VALUE_NONE, + 'Option to overwrite packages if they already exist' + ) + ->addOption( + 'timeout', + 't', + InputOption::VALUE_OPTIONAL, + 'Option to set the timeout in seconds when downloading the update (0 for no timeout)', + 30 + ) + ->setDescription('Detects and performs an update of Grav itself when available') + ->setHelp('The update command updates Grav itself when a new version is available'); + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + if (!class_exists(ZipArchive::class)) { + $io->title('GPM Self Upgrade'); + $io->error('php-zip extension needs to be enabled!'); + + return 1; + } + + $this->upgrader = new Upgrader($input->getOption('force')); + $this->all_yes = $input->getOption('all-yes'); + $this->overwrite = $input->getOption('overwrite'); + $this->timeout = (int) $input->getOption('timeout'); + + $this->displayGPMRelease(); + + $update = $this->upgrader->getAssets()['grav-update']; + + $local = $this->upgrader->getLocalVersion(); + $remote = $this->upgrader->getRemoteVersion(); + $release = strftime('%c', strtotime($this->upgrader->getReleaseDate())); + + if (!$this->upgrader->meetsRequirements()) { + $io->writeln('ATTENTION:'); + $io->writeln(' Grav has increased the minimum PHP requirement.'); + $io->writeln(' You are currently running PHP ' . phpversion() . ', but PHP ' . $this->upgrader->minPHPVersion() . ' is required.'); + $io->writeln(' Additional information: http://getgrav.org/blog/changing-php-requirements'); + $io->newLine(); + $io->writeln('Selfupgrade aborted.'); + $io->newLine(); + + return 1; + } + + if (!$this->overwrite && !$this->upgrader->isUpgradable()) { + $io->writeln("You are already running the latest version of Grav v{$local}"); + $io->writeln("which was released on {$release}"); + + $config = Grav::instance()['config']; + $schema = $config->get('versions.core.grav.schema'); + if ($schema !== GRAV_SCHEMA && version_compare($schema, GRAV_SCHEMA, '<')) { + $io->newLine(); + $io->writeln('However post-install scripts have not been run.'); + if (!$this->all_yes) { + $question = new ConfirmationQuestion( + 'Would you like to run the scripts? [Y|n] ', + true + ); + $answer = $io->askQuestion($question); + } else { + $answer = true; + } + + if ($answer) { + // Finalize installation. + Install::instance()->finalize(); + + $io->write(' |- Running post-install scripts... '); + $io->writeln(" '- Success! "); + $io->newLine(); + } + } + + return 0; + } + + Installer::isValidDestination(GRAV_ROOT . '/system'); + if (Installer::IS_LINK === Installer::lastErrorCode()) { + $io->writeln('ATTENTION: Grav is symlinked, cannot upgrade, aborting...'); + $io->newLine(); + $io->writeln("You are currently running a symbolically linked Grav v{$local}. Latest available is v{$remote}."); + + return 1; + } + + // not used but preloaded just in case! + new ArrayInput([]); + + $io->writeln("Grav v{$remote} is now available [release date: {$release}]."); + $io->writeln('You are currently using v' . GRAV_VERSION . '.'); + + if (!$this->all_yes) { + $question = new ConfirmationQuestion( + 'Would you like to read the changelog before proceeding? [y|N] ', + false + ); + $answer = $io->askQuestion($question); + + if ($answer) { + $changelog = $this->upgrader->getChangelog(GRAV_VERSION); + + $io->newLine(); + foreach ($changelog as $version => $log) { + $title = $version . ' [' . $log['date'] . ']'; + $content = preg_replace_callback('/\d\.\s\[\]\(#(.*)\)/', static function ($match) { + return "\n" . ucfirst($match[1]) . ':'; + }, $log['content']); + + $io->writeln($title); + $io->writeln(str_repeat('-', strlen($title))); + $io->writeln($content); + $io->newLine(); + } + + $question = new ConfirmationQuestion('Press [ENTER] to continue.', true); + $io->askQuestion($question); + } + + $question = new ConfirmationQuestion('Would you like to upgrade now? [y|N] ', false); + $answer = $io->askQuestion($question); + + if (!$answer) { + $io->writeln('Aborting...'); + + return 1; + } + } + + $io->newLine(); + $io->writeln("Preparing to upgrade to v{$remote}.."); + + $io->write(" |- Downloading upgrade [{$this->formatBytes($update['size'])}]... 0%"); + $this->file = $this->download($update); + + $io->write(' |- Installing upgrade... '); + $installation = $this->upgrade(); + + $error = 0; + if (!$installation) { + $io->writeln(" '- Installation failed or aborted."); + $io->newLine(); + $error = 1; + } else { + $io->writeln(" '- Success! "); + $io->newLine(); + } + + if ($this->tmp && is_dir($this->tmp)) { + Folder::delete($this->tmp); + } + + return $error; + } + + /** + * @param array $package + * @return string + */ + private function download(array $package): string + { + $io = $this->getIO(); + + $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true); + $this->tmp = $tmp_dir . '/grav-update-' . uniqid('', false); + $options = [ + 'timeout' => $this->timeout, + ]; + + $output = Response::get($package['download'], $options, [$this, 'progress']); + + Folder::create($this->tmp); + + $io->write("\x0D"); + $io->write(" |- Downloading upgrade [{$this->formatBytes($package['size'])}]... 100%"); + $io->newLine(); + + file_put_contents($this->tmp . DS . $package['name'], $output); + + return $this->tmp . DS . $package['name']; + } + + /** + * @return bool + */ + private function upgrade(): bool + { + $io = $this->getIO(); + + $this->upgradeGrav($this->file); + + $errorCode = Installer::lastErrorCode(); + if ($errorCode) { + $io->write("\x0D"); + // extra white spaces to clear out the buffer properly + $io->writeln(' |- Installing upgrade... error '); + $io->writeln(" | '- " . Installer::lastErrorMsg()); + + return false; + } + + $io->write("\x0D"); + // extra white spaces to clear out the buffer properly + $io->writeln(' |- Installing upgrade... ok '); + + return true; + } + + /** + * @param array $progress + * @return void + */ + public function progress(array $progress): void + { + $io = $this->getIO(); + + $io->write("\x0D"); + $io->write(" |- Downloading upgrade [{$this->formatBytes($progress['filesize']) }]... " . str_pad( + $progress['percent'], + 5, + ' ', + STR_PAD_LEFT + ) . '%'); + } + + /** + * @param int|float $size + * @param int $precision + * @return string + */ + public function formatBytes($size, int $precision = 2): string + { + $base = log($size) / log(1024); + $suffixes = array('', 'k', 'M', 'G', 'T'); + + return round(1024 ** ($base - floor($base)), $precision) . $suffixes[(int)floor($base)]; + } + + /** + * @param string $zip + * @return void + */ + private function upgradeGrav(string $zip): void + { + try { + $folder = Installer::unZip($zip, $this->tmp . '/zip'); + if ($folder === false) { + throw new RuntimeException(Installer::lastErrorMsg()); + } + + $script = $folder . '/system/install.php'; + if ((file_exists($script) && $install = include $script) && is_callable($install)) { + $install($zip); + } else { + throw new RuntimeException('Uploaded archive file is not a valid Grav update package'); + } + } catch (Exception $e) { + Installer::setError($e->getMessage()); + } + } +} diff --git a/system/src/Grav/Console/Gpm/UninstallCommand.php b/system/src/Grav/Console/Gpm/UninstallCommand.php new file mode 100644 index 0000000..cb14c65 --- /dev/null +++ b/system/src/Grav/Console/Gpm/UninstallCommand.php @@ -0,0 +1,312 @@ +setName('uninstall') + ->addOption( + 'all-yes', + 'y', + InputOption::VALUE_NONE, + 'Assumes yes (or best approach) instead of prompting' + ) + ->addArgument( + 'package', + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + 'The package(s) that are desired to be removed. Use the "index" command for a list of packages' + ) + ->setDescription('Performs the uninstallation of plugins and themes') + ->setHelp('The uninstall command allows to uninstall plugins and themes'); + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + $this->gpm = new GPM(); + + $this->all_yes = $input->getOption('all-yes'); + + $packages = array_map('strtolower', $input->getArgument('package')); + $this->data = ['total' => 0, 'not_found' => []]; + + $total = 0; + foreach ($packages as $package) { + $plugin = $this->gpm->getInstalledPlugin($package); + $theme = $this->gpm->getInstalledTheme($package); + if ($plugin || $theme) { + $this->data[strtolower($package)] = $plugin ?: $theme; + $total++; + } else { + $this->data['not_found'][] = $package; + } + } + $this->data['total'] = $total; + + $io->newLine(); + + if (!$this->data['total']) { + $io->writeln('Nothing to uninstall.'); + $io->newLine(); + + return 0; + } + + if (count($this->data['not_found'])) { + $io->writeln('These packages were not found installed: ' . implode( + ', ', + $this->data['not_found'] + ) . ''); + } + + unset($this->data['not_found'], $this->data['total']); + + // Plugins need to be initialized in order to make clearcache to work. + try { + $this->initializePlugins(); + } catch (Throwable $e) { + $io->writeln("Some plugins failed to initialize: {$e->getMessage()}"); + } + + $error = 0; + foreach ($this->data as $slug => $package) { + $io->writeln("Preparing to uninstall {$package->name} [v{$package->version}]"); + + $io->write(' |- Checking destination... '); + $checks = $this->checkDestination($slug, $package); + + if (!$checks) { + $io->writeln(" '- Installation failed or aborted."); + $io->newLine(); + $error = 1; + } else { + $uninstall = $this->uninstallPackage($slug, $package); + + if (!$uninstall) { + $io->writeln(" '- Uninstallation failed or aborted."); + $error = 1; + } else { + $io->writeln(" '- Success! "); + } + } + } + + // clear cache after successful upgrade + $this->clearCache(); + + return $error; + } + + /** + * @param string $slug + * @param Local\Package|Remote\Package $package + * @param bool $is_dependency + * @return bool + */ + private function uninstallPackage($slug, $package, $is_dependency = false): bool + { + $io = $this->getIO(); + + if (!$slug) { + return false; + } + + //check if there are packages that have this as a dependency. Abort and show list + $dependent_packages = $this->gpm->getPackagesThatDependOnPackage($slug); + if (count($dependent_packages) > ($is_dependency ? 1 : 0)) { + $io->newLine(2); + $io->writeln('Uninstallation failed.'); + $io->newLine(); + if (count($dependent_packages) > ($is_dependency ? 2 : 1)) { + $io->writeln('The installed packages ' . implode(', ', $dependent_packages) . ' depends on this package. Please remove those first.'); + } else { + $io->writeln('The installed package ' . implode(', ', $dependent_packages) . ' depends on this package. Please remove it first.'); + } + + $io->newLine(); + return false; + } + + if (isset($package->dependencies)) { + $dependencies = $package->dependencies; + + if ($is_dependency) { + foreach ($dependencies as $key => $dependency) { + if (in_array($dependency['name'], $this->dependencies, true)) { + unset($dependencies[$key]); + } + } + } elseif (count($dependencies) > 0) { + $io->writeln(' `- Dependencies found...'); + $io->newLine(); + } + + foreach ($dependencies as $dependency) { + $this->dependencies[] = $dependency['name']; + + if (is_array($dependency)) { + $dependency = $dependency['name']; + } + if ($dependency === 'grav' || $dependency === 'php') { + continue; + } + + $dependencyPackage = $this->gpm->findPackage($dependency); + + $dependency_exists = $this->packageExists($dependency, $dependencyPackage); + + if ($dependency_exists == Installer::EXISTS) { + $io->writeln("A dependency on {$dependencyPackage->name} [v{$dependencyPackage->version}] was found"); + + $question = new ConfirmationQuestion(" |- Uninstall {$dependencyPackage->name}? [y|N] ", false); + $answer = $this->all_yes ? true : $io->askQuestion($question); + + if ($answer) { + $uninstall = $this->uninstallPackage($dependency, $dependencyPackage, true); + + if (!$uninstall) { + $io->writeln(" '- Uninstallation failed or aborted."); + } else { + $io->writeln(" '- Success! "); + } + $io->newLine(); + } else { + $io->writeln(" '- You decided not to uninstall {$dependencyPackage->name}."); + $io->newLine(); + } + } + } + } + + + $locator = Grav::instance()['locator']; + $path = $locator->findResource($package->package_type . '://' . $slug); + Installer::uninstall($path); + $errorCode = Installer::lastErrorCode(); + + if ($errorCode && $errorCode !== Installer::IS_LINK && $errorCode !== Installer::EXISTS) { + $io->writeln(" |- Uninstalling {$package->name} package... error "); + $io->writeln(" | '- " . Installer::lastErrorMsg() . ''); + + return false; + } + + $message = Installer::getMessage(); + if ($message) { + $io->writeln(" |- {$message}"); + } + + if (!$is_dependency && $this->dependencies) { + $io->writeln("Finishing up uninstalling {$package->name}"); + } + $io->writeln(" |- Uninstalling {$package->name} package... ok "); + + return true; + } + + /** + * @param string $slug + * @param Local\Package|Remote\Package $package + * @return bool + */ + private function checkDestination(string $slug, $package): bool + { + $io = $this->getIO(); + + $exists = $this->packageExists($slug, $package); + + if ($exists === Installer::IS_LINK) { + $io->write("\x0D"); + $io->writeln(' |- Checking destination... symbolic link'); + + if ($this->all_yes) { + $io->writeln(" | '- Skipped automatically."); + + return false; + } + + $question = new ConfirmationQuestion( + " | '- Destination has been detected as symlink, delete symbolic link first? [y|N] ", + false + ); + + $answer = $io->askQuestion($question); + if (!$answer) { + $io->writeln(" | '- You decided not to delete the symlink automatically."); + + return false; + } + } + + $io->write("\x0D"); + $io->writeln(' |- Checking destination... ok'); + + return true; + } + + /** + * Check if package exists + * + * @param string $slug + * @param Local\Package|Remote\Package $package + * @return int + */ + private function packageExists(string $slug, $package): int + { + $path = Grav::instance()['locator']->findResource($package->package_type . '://' . $slug); + Installer::isValidDestination($path); + + return Installer::lastErrorCode(); + } +} diff --git a/system/src/Grav/Console/Gpm/UpdateCommand.php b/system/src/Grav/Console/Gpm/UpdateCommand.php new file mode 100644 index 0000000..300fe61 --- /dev/null +++ b/system/src/Grav/Console/Gpm/UpdateCommand.php @@ -0,0 +1,289 @@ +setName('update') + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force re-fetching the data from remote' + ) + ->addOption( + 'destination', + 'd', + InputOption::VALUE_OPTIONAL, + 'The grav instance location where the updates should be applied to. By default this would be where the grav cli has been launched from', + GRAV_ROOT + ) + ->addOption( + 'all-yes', + 'y', + InputOption::VALUE_NONE, + 'Assumes yes (or best approach) instead of prompting' + ) + ->addOption( + 'overwrite', + 'o', + InputOption::VALUE_NONE, + 'Option to overwrite packages if they already exist' + ) + ->addOption( + 'plugins', + 'p', + InputOption::VALUE_NONE, + 'Update only plugins' + ) + ->addOption( + 'themes', + 't', + InputOption::VALUE_NONE, + 'Update only themes' + ) + ->addArgument( + 'package', + InputArgument::IS_ARRAY | InputArgument::OPTIONAL, + 'The package or packages that is desired to update. By default all available updates will be applied.' + ) + ->setDescription('Detects and performs an update of plugins and themes when available') + ->setHelp('The update command updates plugins and themes when a new version is available'); + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + if (!class_exists(ZipArchive::class)) { + $io->title('GPM Update'); + $io->error('php-zip extension needs to be enabled!'); + + return 1; + } + + $this->upgrader = new Upgrader($input->getOption('force')); + $local = $this->upgrader->getLocalVersion(); + $remote = $this->upgrader->getRemoteVersion(); + if ($local !== $remote) { + $io->writeln('WARNING: A new version of Grav is available. You should update Grav before updating plugins and themes. If you continue without updating Grav, some plugins or themes may stop working.'); + $io->newLine(); + $question = new ConfirmationQuestion('Continue with the update process? [Y|n] ', true); + $answer = $io->askQuestion($question); + + if (!$answer) { + $io->writeln('Update aborted. Exiting...'); + + return 1; + } + } + + $this->gpm = new GPM($input->getOption('force')); + + $this->all_yes = $input->getOption('all-yes'); + $this->overwrite = $input->getOption('overwrite'); + + $this->displayGPMRelease(); + + $this->destination = realpath($input->getOption('destination')); + + if (!Installer::isGravInstance($this->destination)) { + $io->writeln('ERROR: ' . Installer::lastErrorMsg()); + exit; + } + if ($input->getOption('plugins') === false && $input->getOption('themes') === false) { + $list_type = ['plugins' => true, 'themes' => true]; + } else { + $list_type['plugins'] = $input->getOption('plugins'); + $list_type['themes'] = $input->getOption('themes'); + } + + if ($this->overwrite) { + $this->data = $this->gpm->getInstallable($list_type); + $description = ' can be overwritten'; + } else { + $this->data = $this->gpm->getUpdatable($list_type); + $description = ' need updating'; + } + + $only_packages = array_map('strtolower', $input->getArgument('package')); + + if (!$this->overwrite && !$this->data['total']) { + $io->writeln('Nothing to update.'); + + return 0; + } + + $io->write("Found {$this->gpm->countInstalled()} packages installed of which {$this->data['total']}{$description}"); + + $limit_to = $this->userInputPackages($only_packages); + + $io->newLine(); + + unset($this->data['total'], $limit_to['total']); + + + // updates review + $slugs = []; + + $index = 1; + foreach ($this->data as $packages) { + foreach ($packages as $slug => $package) { + if (!array_key_exists($slug, $limit_to) && count($only_packages)) { + continue; + } + + if (!$package->available) { + $package->available = $package->version; + } + + $io->writeln( + // index + str_pad((string)$index++, 2, '0', STR_PAD_LEFT) . '. ' . + // name + '' . str_pad($package->name, 15) . ' ' . + // version + "[v{$package->version} -> v{$package->available}]" + ); + $slugs[] = $slug; + } + } + + if (!$this->all_yes) { + // prompt to continue + $io->newLine(); + $question = new ConfirmationQuestion('Continue with the update process? [Y|n] ', true); + $answer = $io->askQuestion($question); + + if (!$answer) { + $io->writeln('Update aborted. Exiting...'); + + return 1; + } + } + + // finally update + $install_command = $this->getApplication()->find('install'); + + $args = new ArrayInput([ + 'command' => 'install', + 'package' => $slugs, + '-f' => $input->getOption('force'), + '-d' => $this->destination, + '-y' => true + ]); + $command_exec = $install_command->run($args, $io); + + if ($command_exec != 0) { + $io->writeln('Error: An error occurred while trying to install the packages'); + + return 1; + } + + return 0; + } + + /** + * @param array $only_packages + * @return array + */ + private function userInputPackages(array $only_packages): array + { + $io = $this->getIO(); + + $found = ['total' => 0]; + $ignore = []; + + if (!count($only_packages)) { + $io->newLine(); + } else { + foreach ($only_packages as $only_package) { + $find = $this->gpm->findPackage($only_package); + + if (!$find || (!$this->overwrite && !$this->gpm->isUpdatable($find->slug))) { + $name = $find->slug ?? $only_package; + $ignore[$name] = $name; + } else { + $found[$find->slug] = $find; + $found['total']++; + } + } + + if ($found['total']) { + $list = $found; + unset($list['total']); + $list = array_keys($list); + + if ($found['total'] !== $this->data['total']) { + $io->write(", only {$found['total']} will be updated"); + } + + $io->newLine(); + $io->writeln('Limiting updates for only ' . implode( + ', ', + $list + ) . ''); + } + + if (count($ignore)) { + $io->newLine(); + $io->writeln('Packages not found or not requiring updates: ' . implode( + ', ', + $ignore + ) . ''); + } + } + + return $found; + } +} diff --git a/system/src/Grav/Console/Gpm/VersionCommand.php b/system/src/Grav/Console/Gpm/VersionCommand.php new file mode 100644 index 0000000..124aad3 --- /dev/null +++ b/system/src/Grav/Console/Gpm/VersionCommand.php @@ -0,0 +1,125 @@ +setName('version') + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force re-fetching the data from remote' + ) + ->addArgument( + 'package', + InputArgument::IS_ARRAY | InputArgument::OPTIONAL, + 'The package or packages that is desired to know the version of. By default and if not specified this would be grav' + ) + ->setDescription('Shows the version of an installed package. If available also shows pending updates.') + ->setHelp('The version command displays the current version of a package installed and, if available, the available version of pending updates'); + } + + /** + * @return int + */ + protected function serve(): int + { + $input = $this->getInput(); + $io = $this->getIO(); + + $this->gpm = new GPM($input->getOption('force')); + $packages = $input->getArgument('package'); + + $installed = false; + + if (!count($packages)) { + $packages = ['grav']; + } + + foreach ($packages as $package) { + $package = strtolower($package); + $name = null; + $version = null; + $updatable = false; + + if ($package === 'grav') { + $name = 'Grav'; + $version = GRAV_VERSION; + $upgrader = new Upgrader(); + + if ($upgrader->isUpgradable()) { + $updatable = " [upgradable: v{$upgrader->getRemoteVersion()}]"; + } + } else { + // get currently installed version + $locator = Grav::instance()['locator']; + $blueprints_path = $locator->findResource('plugins://' . $package . DS . 'blueprints.yaml'); + if (!file_exists($blueprints_path)) { // theme? + $blueprints_path = $locator->findResource('themes://' . $package . DS . 'blueprints.yaml'); + if (!file_exists($blueprints_path)) { + continue; + } + } + + $file = YamlFile::instance($blueprints_path); + $package_yaml = $file->content(); + $file->free(); + + $version = $package_yaml['version']; + + if (!$version) { + continue; + } + + $installed = $this->gpm->findPackage($package); + if ($installed) { + $name = $installed->name; + + if ($this->gpm->isUpdatable($package)) { + $updatable = " [updatable: v{$installed->available}]"; + } + } + } + + $updatable = $updatable ?: ''; + + if ($installed || $package === 'grav') { + $io->writeln("You are running {$name} v{$version}{$updatable}"); + } else { + $io->writeln("Package {$package} not found"); + } + } + + return 0; + } +} diff --git a/system/src/Grav/Console/GpmCommand.php b/system/src/Grav/Console/GpmCommand.php new file mode 100644 index 0000000..816e872 --- /dev/null +++ b/system/src/Grav/Console/GpmCommand.php @@ -0,0 +1,68 @@ +setupConsole($input, $output); + + $grav = Grav::instance(); + $grav['config']->init(); + $grav['uri']->init(); + // @phpstan-ignore-next-line + $grav['accounts']; + + return $this->serve(); + } + + /** + * Override with your implementation. + * + * @return int + */ + protected function serve() + { + // Return error. + return 1; + } + + /** + * @return void + */ + protected function displayGPMRelease() + { + /** @var Config $config */ + $config = Grav::instance()['config']; + + $io = $this->getIO(); + $io->newLine(); + $io->writeln('GPM Releases Configuration: ' . ucfirst($config->get('system.gpm.releases')) . ''); + $io->newLine(); + } +} diff --git a/system/src/Grav/Console/GravCommand.php b/system/src/Grav/Console/GravCommand.php new file mode 100644 index 0000000..0249f14 --- /dev/null +++ b/system/src/Grav/Console/GravCommand.php @@ -0,0 +1,52 @@ +setupConsole($input, $output); + + // Old versions of Grav called this command after grav upgrade. + // We need make this command to work with older ConsoleTrait: + if (method_exists($this, 'initializeGrav')) { + $this->initializeGrav(); + } + + return $this->serve(); + } + + /** + * Override with your implementation. + * + * @return int + */ + protected function serve() + { + // Return error. + return 1; + } +} diff --git a/system/src/Grav/Console/Plugin/PluginListCommand.php b/system/src/Grav/Console/Plugin/PluginListCommand.php new file mode 100644 index 0000000..81041c0 --- /dev/null +++ b/system/src/Grav/Console/Plugin/PluginListCommand.php @@ -0,0 +1,69 @@ +setHidden(true); + } + + /** + * @return int + */ + protected function serve(): int + { + $bin = $this->argv; + $pattern = '([A-Z]\w+Command\.php)'; + + $io = $this->getIO(); + $io->newLine(); + $io->writeln('Usage:'); + $io->writeln(" {$bin} [slug] [command] [arguments]"); + $io->newLine(); + $io->writeln('Example:'); + $io->writeln(" {$bin} error log -l 1 --trace"); + $io->newLine(); + $io->writeln('Plugins with CLI available:'); + + $plugins = Plugins::all(); + $index = 0; + foreach ($plugins as $name => $plugin) { + if (!$plugin->enabled) { + continue; + } + + $list = Folder::all("plugins://{$name}", ['compare' => 'Pathname', 'pattern' => '/\/cli\/' . $pattern . '$/usm', 'levels' => 1]); + if (!$list) { + continue; + } + + $index++; + $num = str_pad((string)$index, 2, '0', STR_PAD_LEFT); + $io->writeln(' ' . $num . '. ' . str_pad($name, 15) . " {$bin} {$name} list"); + } + + return 0; + } +} diff --git a/system/src/Grav/Console/TerminalObjects/Table.php b/system/src/Grav/Console/TerminalObjects/Table.php new file mode 100644 index 0000000..a71d7c0 --- /dev/null +++ b/system/src/Grav/Console/TerminalObjects/Table.php @@ -0,0 +1,38 @@ +column_widths = $this->getColumnWidths(); + $this->table_width = $this->getWidth(); + $this->border = $this->getBorder(); + + $this->buildHeaderRow(); + + foreach ($this->data as $key => $columns) { + $this->rows[] = $this->buildRow($columns); + } + + $this->rows[] = $this->border; + + return $this->rows; + } +} diff --git a/system/src/Grav/Events/FlexRegisterEvent.php b/system/src/Grav/Events/FlexRegisterEvent.php new file mode 100644 index 0000000..13aebf3 --- /dev/null +++ b/system/src/Grav/Events/FlexRegisterEvent.php @@ -0,0 +1,45 @@ +flex = $flex; + } + + /** + * @return array + */ + public function __debugInfo(): array + { + return (array)$this; + } +} diff --git a/system/src/Grav/Events/PermissionsRegisterEvent.php b/system/src/Grav/Events/PermissionsRegisterEvent.php new file mode 100644 index 0000000..5e63c85 --- /dev/null +++ b/system/src/Grav/Events/PermissionsRegisterEvent.php @@ -0,0 +1,45 @@ +permissions = $permissions; + } + + /** + * @return array + */ + public function __debugInfo(): array + { + return (array)$this; + } +} diff --git a/system/src/Grav/Events/PluginsLoadedEvent.php b/system/src/Grav/Events/PluginsLoadedEvent.php new file mode 100644 index 0000000..9dfd18b --- /dev/null +++ b/system/src/Grav/Events/PluginsLoadedEvent.php @@ -0,0 +1,53 @@ +grav = $grav; + $this->plugins = $plugins; + } + + /** + * @return array + */ + public function __debugInfo(): array + { + return [ + 'plugins' => $this->plugins + ]; + } +} diff --git a/system/src/Grav/Events/SessionStartEvent.php b/system/src/Grav/Events/SessionStartEvent.php new file mode 100644 index 0000000..e724a09 --- /dev/null +++ b/system/src/Grav/Events/SessionStartEvent.php @@ -0,0 +1,36 @@ +start() right after successful session_start() call. + * + * @property SessionInterface $session Session instance. + */ +class SessionStartEvent extends Event +{ + /** @var SessionInterface */ + public $session; + + public function __construct(SessionInterface $session) + { + $this->session = $session; + } + + public function __debugInfo(): array + { + return (array)$this; + } +} diff --git a/system/src/Grav/Framework/Acl/Access.php b/system/src/Grav/Framework/Acl/Access.php new file mode 100644 index 0000000..ccc22cf --- /dev/null +++ b/system/src/Grav/Framework/Acl/Access.php @@ -0,0 +1,236 @@ +name = $name; + $this->rules = $rules ?? []; + $this->ops = ['+' => true, '-' => false]; + if (is_string($acl)) { + $this->acl = $this->resolvePermissions($acl); + } elseif (is_array($acl)) { + $this->acl = $this->normalizeAcl($acl); + } + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param Access $parent + * @param string|null $name + * @return void + */ + public function inherit(Access $parent, string $name = null) + { + // Remove cached null actions from acl. + $acl = $this->getAllActions(); + // Get only inherited actions. + $inherited = array_diff_key($parent->getAllActions(), $acl); + + $this->inherited += $parent->inherited + array_fill_keys(array_keys($inherited), $name ?? $parent->getName()); + $acl = array_replace($acl, $inherited); + if (null === $acl) { + throw new RuntimeException('Internal error'); + } + + $this->acl = $acl; + } + + /** + * Checks user authorization to the action. + * + * @param string $action + * @param string|null $scope + * @return bool|null + */ + public function authorize(string $action, string $scope = null): ?bool + { + if (null !== $scope) { + $action = $scope !== 'test' ? "{$scope}.{$action}" : $action; + } + + return $this->get($action); + } + + /** + * @return array + */ + public function toArray(): array + { + return Utils::arrayUnflattenDotNotation($this->acl); + } + + /** + * @return array + */ + public function getAllActions(): array + { + return array_filter($this->acl, static function($val) { return $val !== null; }); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } + + /** + * @param string $action + * @return bool|null + */ + public function get(string $action) + { + // Get access value. + if (isset($this->acl[$action])) { + return $this->acl[$action]; + } + + // If no value is defined, check the parent access (all true|false). + $pos = strrpos($action, '.'); + $value = $pos ? $this->get(substr($action, 0, $pos)) : null; + + // Cache result for faster lookup. + $this->acl[$action] = $value; + + return $value; + } + + /** + * @param string $action + * @return bool + */ + public function isInherited(string $action): bool + { + return isset($this->inherited[$action]); + } + + /** + * @param string $action + * @return string|null + */ + public function getInherited(string $action): ?string + { + return $this->inherited[$action] ?? null; + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->acl); + } + + /** + * @return int + */ + public function count(): int + { + return count($this->acl); + } + + /** + * @param array $acl + * @return array + */ + protected function normalizeAcl(array $acl): array + { + if (empty($acl)) { + return []; + } + + // Normalize access control list. + $list = []; + foreach (Utils::arrayFlattenDotNotation($acl) as $key => $value) { + if (is_bool($value)) { + $list[$key] = $value; + } elseif ($value === 0 || $value === 1) { + $list[$key] = (bool)$value; + } elseif($value === null) { + continue; + } elseif ($this->rules && is_string($value)) { + $list[$key] = $this->resolvePermissions($value); + } elseif (Utils::isPositive($value)) { + $list[$key] = true; + } elseif (Utils::isNegative($value)) { + $list[$key] = false; + } + } + + return $list; + } + + /** + * @param string $access + * @return array + */ + protected function resolvePermissions(string $access): array + { + $len = strlen($access); + $op = true; + $list = []; + for($count = 0; $count < $len; $count++) { + $letter = $access[$count]; + if (isset($this->rules[$letter])) { + $list[$this->rules[$letter]] = $op; + $op = true; + } elseif (isset($this->ops[$letter])) { + $op = $this->ops[$letter]; + } + } + + return $list; + } +} diff --git a/system/src/Grav/Framework/Acl/Action.php b/system/src/Grav/Framework/Acl/Action.php new file mode 100644 index 0000000..1f883a0 --- /dev/null +++ b/system/src/Grav/Framework/Acl/Action.php @@ -0,0 +1,203 @@ +name = $name; + $this->type = $action['type'] ?? 'action'; + $this->visible = (bool)($action['visible'] ?? true); + $this->label = $label; + unset($action['type'], $action['label']); + $this->params = $action; + + // Include compact rules. + if (isset($action['letters'])) { + foreach ($action['letters'] as $letter => $data) { + $data['letter'] = $letter; + $childName = $this->name . '.' . $data['action']; + unset($data['action']); + $child = new Action($childName, $data); + $this->addChild($child); + } + } + } + + /** + * @return array + */ + public function getParams(): array + { + return $this->params; + } + + /** + * @param string $name + * @return mixed|null + */ + public function getParam(string $name) + { + return $this->params[$name] ?? null; + } + + /** + * @return Action|null + */ + public function getParent(): ?Action + { + return $this->parent; + } + + /** + * @param Action|null $parent + * @return void + */ + public function setParent(?Action $parent): void + { + $this->parent = $parent; + } + + /** + * @return string + */ + public function getScope(): string + { + $pos = strpos($this->name, '.'); + if ($pos) { + return substr($this->name, 0, $pos); + } + + return $this->name; + } + + /** + * @return int + */ + public function getLevels(): int + { + return substr_count($this->name, '.'); + } + + /** + * @return bool + */ + public function hasChildren(): bool + { + return !empty($this->children); + } + + /** + * @return Action[] + */ + public function getChildren(): array + { + return $this->children; + } + + /** + * @param string $name + * @return Action|null + */ + public function getChild(string $name): ?Action + { + return $this->children[$name] ?? null; + } + + /** + * @param Action $child + * @return void + */ + public function addChild(Action $child): void + { + if (strpos($child->name, "{$this->name}.") !== 0) { + throw new RuntimeException('Bad child'); + } + + $child->setParent($this); + $name = substr($child->name, strlen($this->name) + 1); + + $this->children[$name] = $child; + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->children); + } + + /** + * @return int + */ + public function count(): int + { + return count($this->children); + } + + /** + * @return array + */ + public function __debugInfo() + { + return [ + 'name' => $this->name, + 'type' => $this->type, + 'label' => $this->label, + 'params' => $this->params, + 'actions' => $this->children + ]; + } +} diff --git a/system/src/Grav/Framework/Acl/Permissions.php b/system/src/Grav/Framework/Acl/Permissions.php new file mode 100644 index 0000000..0375716 --- /dev/null +++ b/system/src/Grav/Framework/Acl/Permissions.php @@ -0,0 +1,244 @@ +actions); + $recursive = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST); + + return iterator_to_array($recursive); + } + + /** + * @param string $name + * @return bool + */ + public function hasAction(string $name): bool + { + return isset($this->instances[$name]); + } + + /** + * @param string $name + * @return Action|null + */ + public function getAction(string $name): ?Action + { + return $this->instances[$name] ?? null; + } + + /** + * @param Action $action + * @return void + */ + public function addAction(Action $action): void + { + $name = $action->name; + $parent = $this->getParent($name); + if ($parent) { + $parent->addChild($action); + } else { + $this->actions[$name] = $action; + } + + $this->instances[$name] = $action; + + // If Action has children, add those, too. + foreach ($action->getChildren() as $child) { + $this->instances[$child->name] = $child; + } + } + + /** + * @return array + */ + public function getActions(): array + { + return $this->actions; + } + + /** + * @param Action[] $actions + * @return void + */ + public function addActions(array $actions): void + { + foreach ($actions as $action) { + $this->addAction($action); + } + } + + /** + * @param string $name + * @return bool + */ + public function hasType(string $name): bool + { + return isset($this->types[$name]); + } + + /** + * @param string $name + * @return Action|null + */ + public function getType(string $name): ?Action + { + return $this->types[$name] ?? null; + } + + /** + * @param string $name + * @param array $type + * @return void + */ + public function addType(string $name, array $type): void + { + $this->types[$name] = $type; + } + + /** + * @return array + */ + public function getTypes(): array + { + return $this->types; + } + + /** + * @param array $types + * @return void + */ + public function addTypes(array $types): void + { + $types = array_replace($this->types, $types); + if (null === $types) { + throw new RuntimeException('Internal error'); + } + + $this->types = $types; + } + + /** + * @param array|null $access + * @return Access + */ + public function getAccess(array $access = null): Access + { + return new Access($access ?? []); + } + + /** + * @param int|string $offset + * @return bool + */ + public function offsetExists($offset): bool + { + return isset($this->nested[$offset]); + } + + /** + * @param int|string $offset + * @return Action|null + */ + public function offsetGet($offset): ?Action + { + return $this->nested[$offset] ?? null; + } + + /** + * @param int|string $offset + * @param mixed $value + * @return void + */ + public function offsetSet($offset, $value): void + { + throw new RuntimeException(__METHOD__ . '(): Not Supported'); + } + + /** + * @param int|string $offset + * @return void + */ + public function offsetUnset($offset): void + { + throw new RuntimeException(__METHOD__ . '(): Not Supported'); + } + + /** + * @return int + */ + public function count(): int + { + return count($this->actions); + } + + /** + * @return ArrayIterator|Traversable + */ + public function getIterator() + { + return new ArrayIterator($this->actions); + } + + /** + * @return array + */ + public function __debugInfo() + { + return [ + 'actions' => $this->actions + ]; + } + + /** + * @param string $name + * @return Action|null + */ + protected function getParent(string $name): ?Action + { + if ($pos = strrpos($name, '.')) { + $parentName = substr($name, 0, $pos); + + $parent = $this->getAction($parentName); + if (!$parent) { + $parent = new Action($parentName); + $this->addAction($parent); + } + + return $parent; + } + + return null; + } +} diff --git a/system/src/Grav/Framework/Acl/PermissionsReader.php b/system/src/Grav/Framework/Acl/PermissionsReader.php new file mode 100644 index 0000000..b157a5d --- /dev/null +++ b/system/src/Grav/Framework/Acl/PermissionsReader.php @@ -0,0 +1,187 @@ +content(); + $actions = $content['actions'] ?? []; + $types = $content['types'] ?? []; + + return static::fromArray($actions, $types); + } + + /** + * @param array $actions + * @param array $types + * @return Action[] + */ + public static function fromArray(array $actions, array $types): array + { + static::initTypes($types); + + $list = []; + foreach (static::read($actions) as $type => $data) { + $list[$type] = new Action($type, $data); + } + + return $list; + } + + /** + * @param array $actions + * @param string $prefix + * @return array + */ + public static function read(array $actions, string $prefix = ''): array + { + $list = []; + foreach ($actions as $name => $action) { + $prefixNname = $prefix . $name; + $list[$prefixNname] = null; + + // Support nested sets of actions. + if (isset($action['actions']) && is_array($action['actions'])) { + $list += static::read($action['actions'], "{$prefixNname}."); + } + + unset($action['actions']); + + // Add defaults if they exist. + $action = static::addDefaults($action); + + // Build flat list of actions. + $list[$prefixNname] = $action; + } + + return $list; + } + + /** + * @param array $types + * @return void + */ + protected static function initTypes(array $types) + { + static::$types = []; + + $dependencies = []; + foreach ($types as $type => $defaults) { + $current = array_fill_keys((array)($defaults['use'] ?? null), null); + $defType = $defaults['type'] ?? $type; + if ($type !== $defType) { + $current[$defaults['type']] = null; + } + + $dependencies[$type] = (object)$current; + } + + // Build dependency tree. + foreach ($dependencies as $type => $dep) { + foreach (get_object_vars($dep) as $k => &$val) { + if (null === $val) { + $val = $dependencies[$k] ?? new stdClass(); + } + } + unset($val); + } + + $encoded = json_encode($dependencies); + if ($encoded === false) { + throw new RuntimeException('json_encode(): failed to encode dependencies'); + } + $dependencies = json_decode($encoded, true); + + foreach (static::getDependencies($dependencies) as $type) { + $defaults = $types[$type] ?? null; + if ($defaults) { + static::$types[$type] = static::addDefaults($defaults); + } + } + } + + /** + * @param array $dependencies + * @return array + */ + protected static function getDependencies(array $dependencies): array + { + $list = [[]]; + foreach ($dependencies as $name => $deps) { + $current = $deps ? static::getDependencies($deps) : []; + $current[] = $name; + + $list[] = $current; + } + + return array_unique(array_merge(...$list)); + } + + /** + * @param array $action + * @return array + */ + protected static function addDefaults(array $action): array + { + $scopes = []; + + // Add used properties. + $use = (array)($action['use'] ?? null); + foreach ($use as $type) { + if (isset(static::$types[$type])) { + $used = static::$types[$type]; + unset($used['type']); + $scopes[] = $used; + } + } + unset($action['use']); + + // Add type defaults. + $type = $action['type'] ?? 'default'; + $defaults = static::$types[$type] ?? null; + if (is_array($defaults)) { + $scopes[] = $defaults; + } + + if ($scopes) { + $scopes[] = $action; + + $action = array_replace_recursive(...$scopes); + if (null === $action) { + throw new RuntimeException('Internal error'); + } + + $newType = $defaults['type'] ?? null; + if ($newType && $newType !== $type) { + $action['type'] = $newType; + } + } + + return $action; + } +} diff --git a/system/src/Grav/Framework/Acl/RecursiveActionIterator.php b/system/src/Grav/Framework/Acl/RecursiveActionIterator.php new file mode 100644 index 0000000..ac219da --- /dev/null +++ b/system/src/Grav/Framework/Acl/RecursiveActionIterator.php @@ -0,0 +1,60 @@ +current(); + + return $current->name; + } + + /** + * @see \RecursiveIterator::hasChildren() + * @return bool + */ + public function hasChildren(): bool + { + /** @var Action $current */ + $current = $this->current(); + + return $current->hasChildren(); + } + + /** + * @see \RecursiveIterator::getChildren() + * @return RecursiveActionIterator + */ + public function getChildren(): self + { + /** @var Action $current */ + $current = $this->current(); + + return new static($current->getChildren()); + } +} diff --git a/system/src/Grav/Framework/Cache/AbstractCache.php b/system/src/Grav/Framework/Cache/AbstractCache.php new file mode 100644 index 0000000..bfb5125 --- /dev/null +++ b/system/src/Grav/Framework/Cache/AbstractCache.php @@ -0,0 +1,32 @@ +init($namespace, $defaultLifetime); + } +} diff --git a/system/src/Grav/Framework/Cache/Adapter/ChainCache.php b/system/src/Grav/Framework/Cache/Adapter/ChainCache.php new file mode 100644 index 0000000..76f5185 --- /dev/null +++ b/system/src/Grav/Framework/Cache/Adapter/ChainCache.php @@ -0,0 +1,206 @@ +caches = array_values($caches); + $this->count = count($caches); + } + + /** + * @inheritdoc + */ + public function doGet($key, $miss) + { + foreach ($this->caches as $i => $cache) { + $value = $cache->doGet($key, $miss); + if ($value !== $miss) { + while (--$i >= 0) { + // Update all the previous caches with missing value. + $this->caches[$i]->doSet($key, $value, $this->getDefaultLifetime()); + } + + return $value; + } + } + + return $miss; + } + + /** + * @inheritdoc + */ + public function doSet($key, $value, $ttl) + { + $success = true; + $i = $this->count; + + while ($i--) { + $success = $this->caches[$i]->doSet($key, $value, $ttl) && $success; + } + + return $success; + } + + /** + * @inheritdoc + */ + public function doDelete($key) + { + $success = true; + $i = $this->count; + + while ($i--) { + $success = $this->caches[$i]->doDelete($key) && $success; + } + + return $success; + } + + /** + * @inheritdoc + */ + public function doClear() + { + $success = true; + $i = $this->count; + + while ($i--) { + $success = $this->caches[$i]->doClear() && $success; + } + + return $success; + } + + /** + * @inheritdoc + */ + public function doGetMultiple($keys, $miss) + { + $list = []; + /** + * @var int $i + * @var CacheInterface $cache + */ + foreach ($this->caches as $i => $cache) { + $list[$i] = $cache->doGetMultiple($keys, $miss); + + $keys = array_diff_key($keys, $list[$i]); + + if (!$keys) { + break; + } + } + + // Update all the previous caches with missing values. + $values = []; + /** + * @var int $i + * @var CacheInterface $items + */ + foreach (array_reverse($list) as $i => $items) { + $values += $items; + if ($i && $values) { + $this->caches[$i-1]->doSetMultiple($values, $this->getDefaultLifetime()); + } + } + + return $values; + } + + /** + * @inheritdoc + */ + public function doSetMultiple($values, $ttl) + { + $success = true; + $i = $this->count; + + while ($i--) { + $success = $this->caches[$i]->doSetMultiple($values, $ttl) && $success; + } + + return $success; + } + + /** + * @inheritdoc + */ + public function doDeleteMultiple($keys) + { + $success = true; + $i = $this->count; + + while ($i--) { + $success = $this->caches[$i]->doDeleteMultiple($keys) && $success; + } + + return $success; + } + + /** + * @inheritdoc + */ + public function doHas($key) + { + foreach ($this->caches as $cache) { + if ($cache->doHas($key)) { + return true; + } + } + + return false; + } +} diff --git a/system/src/Grav/Framework/Cache/Adapter/DoctrineCache.php b/system/src/Grav/Framework/Cache/Adapter/DoctrineCache.php new file mode 100644 index 0000000..29e9e3b --- /dev/null +++ b/system/src/Grav/Framework/Cache/Adapter/DoctrineCache.php @@ -0,0 +1,114 @@ +getNamespace(); + if ($namespace) { + $doctrineCache->setNamespace($namespace); + } + + $this->driver = $doctrineCache; + } + + /** + * @inheritdoc + */ + public function doGet($key, $miss) + { + $value = $this->driver->fetch($key); + + // Doctrine cache does not differentiate between no result and cached 'false'. Make sure that we do. + return $value !== false || $this->driver->contains($key) ? $value : $miss; + } + + /** + * @inheritdoc + */ + public function doSet($key, $value, $ttl) + { + return $this->driver->save($key, $value, (int) $ttl); + } + + /** + * @inheritdoc + */ + public function doDelete($key) + { + return $this->driver->delete($key); + } + + /** + * @inheritdoc + */ + public function doClear() + { + return $this->driver->deleteAll(); + } + + /** + * @inheritdoc + */ + public function doGetMultiple($keys, $miss) + { + return $this->driver->fetchMultiple($keys); + } + + /** + * @inheritdoc + */ + public function doSetMultiple($values, $ttl) + { + return $this->driver->saveMultiple($values, (int) $ttl); + } + + /** + * @inheritdoc + */ + public function doDeleteMultiple($keys) + { + return $this->driver->deleteMultiple($keys); + } + + /** + * @inheritdoc + */ + public function doHas($key) + { + return $this->driver->contains($key); + } +} diff --git a/system/src/Grav/Framework/Cache/Adapter/FileCache.php b/system/src/Grav/Framework/Cache/Adapter/FileCache.php new file mode 100644 index 0000000..1995e15 --- /dev/null +++ b/system/src/Grav/Framework/Cache/Adapter/FileCache.php @@ -0,0 +1,255 @@ +initFileCache($namespace, $folder ?? ''); + } + + /** + * @inheritdoc + */ + public function doGet($key, $miss) + { + $now = time(); + $file = $this->getFile($key); + + if (!file_exists($file) || !$h = @fopen($file, 'rb')) { + return $miss; + } + + if ($now >= (int) $expiresAt = fgets($h)) { + fclose($h); + @unlink($file); + } else { + $i = rawurldecode(rtrim((string)fgets($h))); + $value = stream_get_contents($h) ?: ''; + fclose($h); + + if ($i === $key) { + return unserialize($value, ['allowed_classes' => true]); + } + } + + return $miss; + } + + /** + * @inheritdoc + * @throws CacheException + */ + public function doSet($key, $value, $ttl) + { + $expiresAt = time() + (int)$ttl; + + $result = $this->write( + $this->getFile($key, true), + $expiresAt . "\n" . rawurlencode($key) . "\n" . serialize($value), + $expiresAt + ); + + if (!$result && !is_writable($this->directory)) { + throw new CacheException(sprintf('Cache directory is not writable (%s)', $this->directory)); + } + + return $result; + } + + /** + * @inheritdoc + */ + public function doDelete($key) + { + $file = $this->getFile($key); + + return (!file_exists($file) || @unlink($file) || !file_exists($file)); + } + + /** + * @inheritdoc + */ + public function doClear() + { + $result = true; + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->directory, FilesystemIterator::SKIP_DOTS)); + + foreach ($iterator as $file) { + $result = ($file->isDir() || @unlink($file) || !file_exists($file)) && $result; + } + + return $result; + } + + /** + * @inheritdoc + */ + public function doHas($key) + { + $file = $this->getFile($key); + + return file_exists($file) && (@filemtime($file) > time() || $this->doGet($key, null)); + } + + /** + * @param string $key + * @param bool $mkdir + * @return string + */ + protected function getFile($key, $mkdir = false) + { + $hash = str_replace('/', '-', base64_encode(hash('sha256', static::class . $key, true))); + $dir = $this->directory . $hash[0] . DIRECTORY_SEPARATOR . $hash[1] . DIRECTORY_SEPARATOR; + + if ($mkdir) { + $this->mkdir($dir); + } + + return $dir . substr($hash, 2, 20); + } + + /** + * @param string $namespace + * @param string $directory + * @return void + * @throws InvalidArgumentException + */ + protected function initFileCache($namespace, $directory) + { + if (!isset($directory[0])) { + $directory = sys_get_temp_dir() . '/grav-cache'; + } else { + $directory = realpath($directory) ?: $directory; + } + + if (isset($namespace[0])) { + if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); + } + $directory .= DIRECTORY_SEPARATOR . $namespace; + } + + $this->mkdir($directory); + + $directory .= DIRECTORY_SEPARATOR; + // On Windows the whole path is limited to 258 chars + if ('\\' === DIRECTORY_SEPARATOR && strlen($directory) > 234) { + throw new InvalidArgumentException(sprintf('Cache folder is too long (%s)', $directory)); + } + $this->directory = $directory; + } + + /** + * @param string $file + * @param string $data + * @param int|null $expiresAt + * @return bool + */ + private function write($file, $data, $expiresAt = null) + { + set_error_handler(__CLASS__.'::throwError'); + + try { + if ($this->tmp === null) { + $this->tmp = $this->directory . uniqid('', true); + } + + file_put_contents($this->tmp, $data); + + if ($expiresAt !== null) { + touch($this->tmp, $expiresAt); + } + + return rename($this->tmp, $file); + } finally { + restore_error_handler(); + } + } + + /** + * @param string $dir + * @return void + * @throws RuntimeException + */ + private function mkdir($dir) + { + // Silence error for open_basedir; should fail in mkdir instead. + if (@is_dir($dir)) { + return; + } + + $success = @mkdir($dir, 0777, true); + + if (!$success) { + // Take yet another look, make sure that the folder doesn't exist. + clearstatcache(true, $dir); + if (!@is_dir($dir)) { + throw new RuntimeException(sprintf('Unable to create directory: %s', $dir)); + } + } + } + + /** + * @param int $type + * @param string $message + * @param string $file + * @param int $line + * @return bool + * @internal + * @throws ErrorException + */ + public static function throwError($type, $message, $file, $line) + { + throw new ErrorException($message, 0, $type, $file, $line); + } + + /** + * @return void + */ + public function __destruct() + { + if ($this->tmp !== null && file_exists($this->tmp)) { + unlink($this->tmp); + } + } +} diff --git a/system/src/Grav/Framework/Cache/Adapter/MemoryCache.php b/system/src/Grav/Framework/Cache/Adapter/MemoryCache.php new file mode 100644 index 0000000..c043bf9 --- /dev/null +++ b/system/src/Grav/Framework/Cache/Adapter/MemoryCache.php @@ -0,0 +1,83 @@ +cache)) { + return $miss; + } + + return $this->cache[$key]; + } + + /** + * @param string $key + * @param mixed $value + * @param int $ttl + * @return bool + */ + public function doSet($key, $value, $ttl) + { + $this->cache[$key] = $value; + + return true; + } + + /** + * @param string $key + * @return bool + */ + public function doDelete($key) + { + unset($this->cache[$key]); + + return true; + } + + /** + * @return bool + */ + public function doClear() + { + $this->cache = []; + + return true; + } + + /** + * @param string $key + * @return bool + */ + public function doHas($key) + { + return array_key_exists($key, $this->cache); + } +} diff --git a/system/src/Grav/Framework/Cache/Adapter/SessionCache.php b/system/src/Grav/Framework/Cache/Adapter/SessionCache.php new file mode 100644 index 0000000..e189e3e --- /dev/null +++ b/system/src/Grav/Framework/Cache/Adapter/SessionCache.php @@ -0,0 +1,107 @@ +doGetStored($key); + + return $stored ? $stored[self::VALUE] : $miss; + } + + /** + * @param string $key + * @param mixed $value + * @param int $ttl + * @return bool + */ + public function doSet($key, $value, $ttl) + { + $stored = [self::VALUE => $value]; + if (null !== $ttl) { + $stored[self::LIFETIME] = time() + $ttl; + } + + $_SESSION[$this->getNamespace()][$key] = $stored; + + return true; + } + + /** + * @param string $key + * @return bool + */ + public function doDelete($key) + { + unset($_SESSION[$this->getNamespace()][$key]); + + return true; + } + + /** + * @return bool + */ + public function doClear() + { + unset($_SESSION[$this->getNamespace()]); + + return true; + } + + /** + * @param string $key + * @return bool + */ + public function doHas($key) + { + return $this->doGetStored($key) !== null; + } + + /** + * @return string + */ + public function getNamespace() + { + return 'cache-' . parent::getNamespace(); + } + + /** + * @param string $key + * @return mixed|null + */ + protected function doGetStored($key) + { + $stored = $_SESSION[$this->getNamespace()][$key] ?? null; + + if (isset($stored[self::LIFETIME]) && $stored[self::LIFETIME] < time()) { + unset($_SESSION[$this->getNamespace()][$key]); + $stored = null; + } + + return $stored ?: null; + } +} diff --git a/system/src/Grav/Framework/Cache/CacheInterface.php b/system/src/Grav/Framework/Cache/CacheInterface.php new file mode 100644 index 0000000..efd9d31 --- /dev/null +++ b/system/src/Grav/Framework/Cache/CacheInterface.php @@ -0,0 +1,71 @@ + $values + * @param int|null $ttl + * @return mixed + */ + public function doSetMultiple($values, $ttl); + + /** + * @param string[] $keys + * @return mixed + */ + public function doDeleteMultiple($keys); + + /** + * @param string $key + * @return mixed + */ + public function doHas($key); +} diff --git a/system/src/Grav/Framework/Cache/CacheTrait.php b/system/src/Grav/Framework/Cache/CacheTrait.php new file mode 100644 index 0000000..287f924 --- /dev/null +++ b/system/src/Grav/Framework/Cache/CacheTrait.php @@ -0,0 +1,376 @@ +namespace = (string) $namespace; + $this->defaultLifetime = $this->convertTtl($defaultLifetime); + $this->miss = new stdClass; + } + + /** + * @param bool $validation + * @return void + */ + public function setValidation($validation) + { + $this->validation = (bool) $validation; + } + + /** + * @return string + */ + protected function getNamespace() + { + return $this->namespace; + } + + /** + * @return int|null + */ + protected function getDefaultLifetime() + { + return $this->defaultLifetime; + } + + /** + * @param string $key + * @param mixed|null $default + * @return mixed|null + * @throws InvalidArgumentException + */ + public function get($key, $default = null) + { + $this->validateKey($key); + + $value = $this->doGet($key, $this->miss); + + return $value !== $this->miss ? $value : $default; + } + + /** + * @param string $key + * @param mixed $value + * @param null|int|DateInterval $ttl + * @return bool + * @throws InvalidArgumentException + */ + public function set($key, $value, $ttl = null) + { + $this->validateKey($key); + + $ttl = $this->convertTtl($ttl); + + // If a negative or zero TTL is provided, the item MUST be deleted from the cache. + return null !== $ttl && $ttl <= 0 ? $this->doDelete($key) : $this->doSet($key, $value, $ttl); + } + + /** + * @param string $key + * @return bool + * @throws InvalidArgumentException + */ + public function delete($key) + { + $this->validateKey($key); + + return $this->doDelete($key); + } + + /** + * @return bool + */ + public function clear() + { + return $this->doClear(); + } + + /** + * @param iterable $keys + * @param mixed|null $default + * @return iterable + * @throws InvalidArgumentException + */ + public function getMultiple($keys, $default = null) + { + if ($keys instanceof Traversable) { + $keys = iterator_to_array($keys, false); + } elseif (!is_array($keys)) { + $isObject = is_object($keys); + throw new InvalidArgumentException( + sprintf( + 'Cache keys must be array or Traversable, "%s" given', + $isObject ? get_class($keys) : gettype($keys) + ) + ); + } + + if (empty($keys)) { + return []; + } + + $this->validateKeys($keys); + $keys = array_unique($keys); + $keys = array_combine($keys, $keys); + if (empty($keys)) { + return []; + } + + $list = $this->doGetMultiple($keys, $this->miss); + + // Make sure that values are returned in the same order as the keys were given. + $values = []; + foreach ($keys as $key) { + if (!array_key_exists($key, $list) || $list[$key] === $this->miss) { + $values[$key] = $default; + } else { + $values[$key] = $list[$key]; + } + } + + return $values; + } + + /** + * @param iterable $values + * @param null|int|DateInterval $ttl + * @return bool + * @throws InvalidArgumentException + */ + public function setMultiple($values, $ttl = null) + { + if ($values instanceof Traversable) { + $values = iterator_to_array($values, true); + } elseif (!is_array($values)) { + $isObject = is_object($values); + throw new InvalidArgumentException( + sprintf( + 'Cache values must be array or Traversable, "%s" given', + $isObject ? get_class($values) : gettype($values) + ) + ); + } + + $keys = array_keys($values); + + if (empty($keys)) { + return true; + } + + $this->validateKeys($keys); + + $ttl = $this->convertTtl($ttl); + + // If a negative or zero TTL is provided, the item MUST be deleted from the cache. + return null !== $ttl && $ttl <= 0 ? $this->doDeleteMultiple($keys) : $this->doSetMultiple($values, $ttl); + } + + /** + * @param iterable $keys + * @return bool + * @throws InvalidArgumentException + */ + public function deleteMultiple($keys) + { + if ($keys instanceof Traversable) { + $keys = iterator_to_array($keys, false); + } elseif (!is_array($keys)) { + $isObject = is_object($keys); + throw new InvalidArgumentException( + sprintf( + 'Cache keys must be array or Traversable, "%s" given', + $isObject ? get_class($keys) : gettype($keys) + ) + ); + } + + if (empty($keys)) { + return true; + } + + $this->validateKeys($keys); + + return $this->doDeleteMultiple($keys); + } + + /** + * @param string $key + * @return bool + * @throws InvalidArgumentException + */ + public function has($key) + { + $this->validateKey($key); + + return $this->doHas($key); + } + + /** + * @param array $keys + * @param mixed $miss + * @return array + */ + public function doGetMultiple($keys, $miss) + { + $results = []; + + foreach ($keys as $key) { + $value = $this->doGet($key, $miss); + if ($value !== $miss) { + $results[$key] = $value; + } + } + + return $results; + } + + /** + * @param array $values + * @param int|null $ttl + * @return bool + */ + public function doSetMultiple($values, $ttl) + { + $success = true; + + foreach ($values as $key => $value) { + $success = $this->doSet($key, $value, $ttl) && $success; + } + + return $success; + } + + /** + * @param array $keys + * @return bool + */ + public function doDeleteMultiple($keys) + { + $success = true; + + foreach ($keys as $key) { + $success = $this->doDelete($key) && $success; + } + + return $success; + } + + /** + * @param string|mixed $key + * @return void + * @throws InvalidArgumentException + */ + protected function validateKey($key) + { + if (!is_string($key)) { + throw new InvalidArgumentException( + sprintf( + 'Cache key must be string, "%s" given', + is_object($key) ? get_class($key) : gettype($key) + ) + ); + } + if (!isset($key[0])) { + throw new InvalidArgumentException('Cache key length must be greater than zero'); + } + if (strlen($key) > 64) { + throw new InvalidArgumentException( + sprintf('Cache key length must be less than 65 characters, key had %d characters', strlen($key)) + ); + } + if (strpbrk($key, '{}()/\@:') !== false) { + throw new InvalidArgumentException( + sprintf('Cache key "%s" contains reserved characters {}()/\@:', $key) + ); + } + } + + /** + * @param array $keys + * @return void + * @throws InvalidArgumentException + */ + protected function validateKeys($keys) + { + if (!$this->validation) { + return; + } + + foreach ($keys as $key) { + $this->validateKey($key); + } + } + + /** + * @param null|int|DateInterval $ttl + * @return int|null + * @throws InvalidArgumentException + */ + protected function convertTtl($ttl) + { + if ($ttl === null) { + return $this->getDefaultLifetime(); + } + + if (is_int($ttl)) { + return $ttl; + } + + if ($ttl instanceof DateInterval) { + $date = DateTime::createFromFormat('U', '0'); + $ttl = $date ? (int)$date->add($ttl)->format('U') : 0; + } + + throw new InvalidArgumentException( + sprintf( + 'Expiration date must be an integer, a DateInterval or null, "%s" given', + is_object($ttl) ? get_class($ttl) : gettype($ttl) + ) + ); + } +} diff --git a/system/src/Grav/Framework/Cache/Exception/CacheException.php b/system/src/Grav/Framework/Cache/Exception/CacheException.php new file mode 100644 index 0000000..1db3256 --- /dev/null +++ b/system/src/Grav/Framework/Cache/Exception/CacheException.php @@ -0,0 +1,21 @@ + + * @implements FileCollectionInterface + */ +class AbstractFileCollection extends AbstractLazyCollection implements FileCollectionInterface +{ + /** @var string */ + protected $path; + /** @var RecursiveDirectoryIterator|RecursiveUniformResourceIterator */ + protected $iterator; + /** @var callable */ + protected $createObjectFunction; + /** @var callable|null */ + protected $filterFunction; + /** @var int */ + protected $flags; + /** @var int */ + protected $nestingLimit; + + /** + * @param string $path + */ + protected function __construct($path) + { + $this->path = $path; + $this->flags = self::INCLUDE_FILES | self::INCLUDE_FOLDERS; + $this->nestingLimit = 0; + $this->createObjectFunction = [$this, 'createObject']; + + $this->setIterator(); + } + + /** + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * @param Criteria $criteria + * @return ArrayCollection + * @todo Implement lazy matching + */ + public function matching(Criteria $criteria) + { + $expr = $criteria->getWhereExpression(); + + $oldFilter = $this->filterFunction; + if ($expr) { + $visitor = new ClosureExpressionVisitor(); + $filter = $visitor->dispatch($expr); + $this->addFilter($filter); + } + + $filtered = $this->doInitializeByIterator($this->iterator, $this->nestingLimit); + $this->filterFunction = $oldFilter; + + if ($orderings = $criteria->getOrderings()) { + $next = null; + /** + * @var string $field + * @var string $ordering + */ + foreach (array_reverse($orderings) as $field => $ordering) { + $next = ClosureExpressionVisitor::sortByField($field, $ordering === Criteria::DESC ? -1 : 1, $next); + } + if (null === $next) { + throw new RuntimeException('Criteria is missing orderings'); + } + + uasort($filtered, $next); + } else { + ksort($filtered); + } + + $offset = $criteria->getFirstResult(); + $length = $criteria->getMaxResults(); + + if ($offset || $length) { + $filtered = array_slice($filtered, (int)$offset, $length); + } + + return new ArrayCollection($filtered); + } + + /** + * @return void + */ + protected function setIterator() + { + $iteratorFlags = RecursiveDirectoryIterator::SKIP_DOTS + FilesystemIterator::UNIX_PATHS + + FilesystemIterator::CURRENT_AS_SELF + FilesystemIterator::FOLLOW_SYMLINKS; + + if (strpos($this->path, '://')) { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $this->iterator = $locator->getRecursiveIterator($this->path, $iteratorFlags); + } else { + $this->iterator = new RecursiveDirectoryIterator($this->path, $iteratorFlags); + } + } + + /** + * @param callable $filterFunction + * @return $this + */ + protected function addFilter(callable $filterFunction) + { + if ($this->filterFunction) { + $oldFilterFunction = $this->filterFunction; + $this->filterFunction = function ($expr) use ($oldFilterFunction, $filterFunction) { + return $oldFilterFunction($expr) && $filterFunction($expr); + }; + } else { + $this->filterFunction = $filterFunction; + } + + return $this; + } + + /** + * {@inheritDoc} + */ + protected function doInitialize() + { + $filtered = $this->doInitializeByIterator($this->iterator, $this->nestingLimit); + ksort($filtered); + + $this->collection = new ArrayCollection($filtered); + } + + /** + * @param SeekableIterator $iterator + * @param int $nestingLimit + * @return array + */ + protected function doInitializeByIterator(SeekableIterator $iterator, $nestingLimit) + { + $children = []; + $objects = []; + $filter = $this->filterFunction; + $objectFunction = $this->createObjectFunction; + + /** @var RecursiveDirectoryIterator $file */ + foreach ($iterator as $file) { + // Skip files if they shouldn't be included. + if (!($this->flags & static::INCLUDE_FILES) && $file->isFile()) { + continue; + } + + // Apply main filter. + if ($filter && !$filter($file)) { + continue; + } + + // Include children if the recursive flag is set. + if (($this->flags & static::RECURSIVE) && $nestingLimit > 0 && $file->hasChildren()) { + $children[] = $file->getChildren(); + } + + // Skip folders if they shouldn't be included. + if (!($this->flags & static::INCLUDE_FOLDERS) && $file->isDir()) { + continue; + } + + $object = $objectFunction($file); + $objects[$object->key] = $object; + } + + if ($children) { + $objects += $this->doInitializeChildren($children, $nestingLimit - 1); + } + + return $objects; + } + + /** + * @param array $children + * @param int $nestingLimit + * @return array + */ + protected function doInitializeChildren(array $children, $nestingLimit) + { + $objects = []; + + foreach ($children as $iterator) { + $objects += $this->doInitializeByIterator($iterator, $nestingLimit); + } + + return $objects; + } + + /** + * @param RecursiveDirectoryIterator $file + * @return object + */ + protected function createObject($file) + { + return (object) [ + 'key' => $file->getSubPathname(), + 'type' => $file->isDir() ? 'folder' : 'file:' . $file->getExtension(), + 'url' => method_exists($file, 'getUrl') ? $file->getUrl() : null, + 'pathname' => $file->getPathname(), + 'mtime' => $file->getMTime() + ]; + } +} diff --git a/system/src/Grav/Framework/Collection/AbstractIndexCollection.php b/system/src/Grav/Framework/Collection/AbstractIndexCollection.php new file mode 100644 index 0000000..25d8672 --- /dev/null +++ b/system/src/Grav/Framework/Collection/AbstractIndexCollection.php @@ -0,0 +1,550 @@ + + */ +abstract class AbstractIndexCollection implements CollectionInterface +{ + use Serializable; + + /** + * @var array + * @phpstan-var array + */ + private $entries; + + /** + * Initializes a new IndexCollection. + * + * @param array $entries + * @phpstan-param array $entries + */ + public function __construct(array $entries = []) + { + $this->entries = $entries; + } + + /** + * {@inheritDoc} + */ + public function toArray() + { + return $this->loadElements($this->entries); + } + + /** + * {@inheritDoc} + */ + public function first() + { + $value = reset($this->entries); + $key = (string)key($this->entries); + + return $this->loadElement($key, $value); + } + + /** + * {@inheritDoc} + */ + public function last() + { + $value = end($this->entries); + $key = (string)key($this->entries); + + return $this->loadElement($key, $value); + } + + /** + * {@inheritDoc} + */ + public function key() + { + /** @phpstan-var TKey $key */ + $key = (string)key($this->entries); + + return $key; + } + + /** + * {@inheritDoc} + */ + public function next() + { + $value = next($this->entries); + $key = (string)key($this->entries); + + return $this->loadElement($key, $value); + } + + /** + * {@inheritDoc} + */ + public function current() + { + $value = current($this->entries); + $key = (string)key($this->entries); + + return $this->loadElement($key, $value); + } + + /** + * {@inheritDoc} + */ + public function remove($key) + { + if (!array_key_exists($key, $this->entries)) { + return null; + } + + $value = $this->entries[$key]; + unset($this->entries[$key]); + + return $this->loadElement((string)$key, $value); + } + + /** + * {@inheritDoc} + */ + public function removeElement($element) + { + $key = $this->isAllowedElement($element) ? $this->getCurrentKey($element) : null; + + if (!$key || !isset($this->entries[$key])) { + return false; + } + + unset($this->entries[$key]); + + return true; + } + + /** + * Required by interface ArrayAccess. + * + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + return $this->containsKey($offset); + } + + /** + * Required by interface ArrayAccess. + * + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * Required by interface ArrayAccess. + * + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + if (null === $offset) { + $this->add($value); + } + + $this->set($offset, $value); + } + + /** + * Required by interface ArrayAccess. + * + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + $this->remove($offset); + } + + /** + * {@inheritDoc} + */ + public function containsKey($key) + { + return isset($this->entries[$key]) || array_key_exists($key, $this->entries); + } + + /** + * {@inheritDoc} + */ + public function contains($element) + { + $key = $this->isAllowedElement($element) ? $this->getCurrentKey($element) : null; + + return $key && isset($this->entries[$key]); + } + + /** + * {@inheritDoc} + */ + public function exists(Closure $p) + { + return $this->loadCollection($this->entries)->exists($p); + } + + /** + * {@inheritDoc} + */ + public function indexOf($element) + { + $key = $this->isAllowedElement($element) ? $this->getCurrentKey($element) : null; + + return $key && isset($this->entries[$key]) ? $key : false; + } + + /** + * {@inheritDoc} + */ + public function get($key) + { + if (!isset($this->entries[$key])) { + return null; + } + + return $this->loadElement((string)$key, $this->entries[$key]); + } + + /** + * {@inheritDoc} + */ + public function getKeys() + { + return array_keys($this->entries); + } + + /** + * {@inheritDoc} + */ + public function getValues() + { + return array_values($this->loadElements($this->entries)); + } + + /** + * {@inheritDoc} + */ + public function count() + { + return count($this->entries); + } + + /** + * {@inheritDoc} + */ + public function set($key, $value) + { + if (!$this->isAllowedElement($value)) { + throw new InvalidArgumentException('Invalid argument $value'); + } + + $this->entries[$key] = $this->getElementMeta($value); + } + + /** + * {@inheritDoc} + */ + public function add($element) + { + if (!$this->isAllowedElement($element)) { + throw new InvalidArgumentException('Invalid argument $element'); + } + + $this->entries[$this->getCurrentKey($element)] = $this->getElementMeta($element); + + return true; + } + + /** + * {@inheritDoc} + */ + public function isEmpty() + { + return empty($this->entries); + } + + /** + * Required by interface IteratorAggregate. + * + * {@inheritDoc} + */ + public function getIterator() + { + return new ArrayIterator($this->loadElements()); + } + + /** + * {@inheritDoc} + */ + public function map(Closure $func) + { + return $this->loadCollection($this->entries)->map($func); + } + + /** + * {@inheritDoc} + */ + public function filter(Closure $p) + { + return $this->loadCollection($this->entries)->filter($p); + } + + /** + * {@inheritDoc} + */ + public function forAll(Closure $p) + { + return $this->loadCollection($this->entries)->forAll($p); + } + + /** + * {@inheritDoc} + */ + public function partition(Closure $p) + { + return $this->loadCollection($this->entries)->partition($p); + } + + /** + * Returns a string representation of this object. + * + * @return string + */ + public function __toString() + { + return __CLASS__ . '@' . spl_object_hash($this); + } + + /** + * {@inheritDoc} + */ + public function clear() + { + $this->entries = []; + } + + /** + * {@inheritDoc} + */ + public function slice($offset, $length = null) + { + return $this->loadElements(array_slice($this->entries, $offset, $length, true)); + } + + /** + * @param int $start + * @param int|null $limit + * @return static + * @phpstan-return static + */ + public function limit($start, $limit = null) + { + return $this->createFrom(array_slice($this->entries, $start, $limit, true)); + } + + /** + * Reverse the order of the items. + * + * @return static + * @phpstan-return static + */ + public function reverse() + { + return $this->createFrom(array_reverse($this->entries)); + } + + /** + * Shuffle items. + * + * @return static + * @phpstan-return static + */ + public function shuffle() + { + $keys = $this->getKeys(); + shuffle($keys); + + return $this->createFrom(array_replace(array_flip($keys), $this->entries) ?? []); + } + + /** + * Select items from collection. + * + * Collection is returned in the order of $keys given to the function. + * + * @param array $keys + * @return static + * @phpstan-return static + */ + public function select(array $keys) + { + $list = []; + foreach ($keys as $key) { + if (isset($this->entries[$key])) { + $list[$key] = $this->entries[$key]; + } + } + + return $this->createFrom($list); + } + + /** + * Un-select items from collection. + * + * @param array $keys + * @return static + * @phpstan-return static + */ + public function unselect(array $keys) + { + return $this->select(array_diff($this->getKeys(), $keys)); + } + + /** + * Split collection into chunks. + * + * @param int $size Size of each chunk. + * @return array + */ + public function chunk($size) + { + return $this->loadCollection($this->entries)->chunk($size); + } + + /** + * @return array + */ + public function __serialize(): array + { + return [ + 'entries' => $this->entries + ]; + } + + /** + * @param array $data + * @return void + */ + public function __unserialize(array $data): void + { + $this->entries = $data['entries']; + } + + /** + * Implements JsonSerializable interface. + * + * @return array + */ + public function jsonSerialize() + { + return $this->loadCollection()->jsonSerialize(); + } + + /** + * Creates a new instance from the specified elements. + * + * This method is provided for derived classes to specify how a new + * instance should be created when constructor semantics have changed. + * + * @param array $entries Elements. + * @return static + * @phpstan-return static + */ + protected function createFrom(array $entries) + { + return new static($entries); + } + + /** + * @return array + */ + protected function getEntries(): array + { + return $this->entries; + } + + /** + * @param array $entries + * @return void + * @phpstan-param array $entries + */ + protected function setEntries(array $entries): void + { + $this->entries = $entries; + } + + /** + * @param FlexObjectInterface $element + * @return string + * @phpstan-param T $element + * @phpstan-return TKey + */ + protected function getCurrentKey($element) + { + return $element->getKey(); + } + + /** + * @param string $key + * @param mixed $value + * @return mixed|null + */ + abstract protected function loadElement($key, $value); + + /** + * @param array|null $entries + * @return array + * @phpstan-return array + */ + abstract protected function loadElements(array $entries = null): array; + + /** + * @param array|null $entries + * @return CollectionInterface + * @phpstan-return C + */ + abstract protected function loadCollection(array $entries = null): CollectionInterface; + + /** + * @param mixed $value + * @return bool + */ + abstract protected function isAllowedElement($value): bool; + + /** + * @param mixed $element + * @return mixed + */ + abstract protected function getElementMeta($element); +} diff --git a/system/src/Grav/Framework/Collection/AbstractLazyCollection.php b/system/src/Grav/Framework/Collection/AbstractLazyCollection.php new file mode 100644 index 0000000..9afaab1 --- /dev/null +++ b/system/src/Grav/Framework/Collection/AbstractLazyCollection.php @@ -0,0 +1,87 @@ + + * @implements CollectionInterface + */ +abstract class AbstractLazyCollection extends BaseAbstractLazyCollection implements CollectionInterface +{ + /** @var ArrayCollection The backed collection to use */ + protected $collection; + + /** + * {@inheritDoc} + */ + public function reverse() + { + $this->initialize(); + + return $this->collection->reverse(); + } + + /** + * {@inheritDoc} + */ + public function shuffle() + { + $this->initialize(); + + return $this->collection->shuffle(); + } + + /** + * {@inheritDoc} + */ + public function chunk($size) + { + $this->initialize(); + + return $this->collection->chunk($size); + } + + /** + * {@inheritDoc} + */ + public function select(array $keys) + { + $this->initialize(); + + return $this->collection->select($keys); + } + + /** + * {@inheritDoc} + */ + public function unselect(array $keys) + { + $this->initialize(); + + return $this->collection->unselect($keys); + } + + /** + * @return array + */ + public function jsonSerialize() + { + $this->initialize(); + + return $this->collection->jsonSerialize(); + } +} diff --git a/system/src/Grav/Framework/Collection/ArrayCollection.php b/system/src/Grav/Framework/Collection/ArrayCollection.php new file mode 100644 index 0000000..d76aa05 --- /dev/null +++ b/system/src/Grav/Framework/Collection/ArrayCollection.php @@ -0,0 +1,104 @@ + + * @implements CollectionInterface + */ +class ArrayCollection extends BaseArrayCollection implements CollectionInterface +{ + /** + * Reverse the order of the items. + * + * @return static + * @phpstan-return static + */ + public function reverse() + { + return $this->createFrom(array_reverse($this->toArray())); + } + + /** + * Shuffle items. + * + * @return static + * @phpstan-return static + */ + public function shuffle() + { + $keys = $this->getKeys(); + shuffle($keys); + + return $this->createFrom(array_replace(array_flip($keys), $this->toArray()) ?? []); + } + + /** + * Split collection into chunks. + * + * @param int $size Size of each chunk. + * @return array + */ + public function chunk($size) + { + return array_chunk($this->toArray(), $size, true); + } + + /** + * Select items from collection. + * + * Collection is returned in the order of $keys given to the function. + * + * @param array $keys + * @return static + * @phpstan-param array $keys + * @phpstan-return static + */ + public function select(array $keys) + { + $list = []; + foreach ($keys as $key) { + if ($this->containsKey($key)) { + $list[$key] = $this->get($key); + } + } + + return $this->createFrom($list); + } + + /** + * Un-select items from collection. + * + * @param array $keys + * @return static + * @phpstan-return static + */ + public function unselect(array $keys) + { + return $this->select(array_diff($this->getKeys(), $keys)); + } + + /** + * Implements JsonSerializable interface. + * + * @return array + */ + public function jsonSerialize() + { + return $this->toArray(); + } +} diff --git a/system/src/Grav/Framework/Collection/CollectionInterface.php b/system/src/Grav/Framework/Collection/CollectionInterface.php new file mode 100644 index 0000000..0739109 --- /dev/null +++ b/system/src/Grav/Framework/Collection/CollectionInterface.php @@ -0,0 +1,68 @@ + + */ +interface CollectionInterface extends Collection, JsonSerializable +{ + /** + * Reverse the order of the items. + * + * @return CollectionInterface + * @phpstan-return static + */ + public function reverse(); + + /** + * Shuffle items. + * + * @return CollectionInterface + * @phpstan-return static + */ + public function shuffle(); + + /** + * Split collection into chunks. + * + * @param int $size Size of each chunk. + * @return array + */ + public function chunk($size); + + /** + * Select items from collection. + * + * Collection is returned in the order of $keys given to the function. + * + * @param array $keys + * @return CollectionInterface + * @phpstan-return static + */ + public function select(array $keys); + + /** + * Un-select items from collection. + * + * @param array $keys + * @return CollectionInterface + * @phpstan-return static + */ + public function unselect(array $keys); +} diff --git a/system/src/Grav/Framework/Collection/FileCollection.php b/system/src/Grav/Framework/Collection/FileCollection.php new file mode 100644 index 0000000..f7b4f25 --- /dev/null +++ b/system/src/Grav/Framework/Collection/FileCollection.php @@ -0,0 +1,97 @@ + + */ +class FileCollection extends AbstractFileCollection +{ + /** + * @param string $path + * @param int $flags + */ + public function __construct($path, $flags = null) + { + parent::__construct($path); + + $this->flags = (int)($flags ?: self::INCLUDE_FILES | self::INCLUDE_FOLDERS | self::RECURSIVE); + + $this->setIterator(); + $this->setFilter(); + $this->setObjectBuilder(); + $this->setNestingLimit(); + } + + /** + * @return int + */ + public function getFlags() + { + return $this->flags; + } + + /** + * @return int + */ + public function getNestingLimit() + { + return $this->nestingLimit; + } + + /** + * @param int $limit + * @return $this + */ + public function setNestingLimit($limit = 99) + { + $this->nestingLimit = (int) $limit; + + return $this; + } + + /** + * @param callable|null $filterFunction + * @return $this + */ + public function setFilter(callable $filterFunction = null) + { + $this->filterFunction = $filterFunction; + + return $this; + } + + /** + * @param callable $filterFunction + * @return $this + */ + public function addFilter(callable $filterFunction) + { + parent::addFilter($filterFunction); + + return $this; + } + + /** + * @param callable|null $objectFunction + * @return $this + */ + public function setObjectBuilder(callable $objectFunction = null) + { + $this->createObjectFunction = $objectFunction ?: [$this, 'createObject']; + + return $this; + } +} diff --git a/system/src/Grav/Framework/Collection/FileCollectionInterface.php b/system/src/Grav/Framework/Collection/FileCollectionInterface.php new file mode 100644 index 0000000..45c446c --- /dev/null +++ b/system/src/Grav/Framework/Collection/FileCollectionInterface.php @@ -0,0 +1,33 @@ + + * @extends Selectable + */ +interface FileCollectionInterface extends CollectionInterface, Selectable +{ + public const INCLUDE_FILES = 1; + public const INCLUDE_FOLDERS = 2; + public const RECURSIVE = 4; + + /** + * @return string + */ + public function getPath(); +} diff --git a/system/src/Grav/Framework/Compat/Serializable.php b/system/src/Grav/Framework/Compat/Serializable.php new file mode 100644 index 0000000..3c9bf6a --- /dev/null +++ b/system/src/Grav/Framework/Compat/Serializable.php @@ -0,0 +1,47 @@ +__serialize()); + } + + /** + * @param string $serialized + * @return void + */ + final public function unserialize($serialized): void + { + $this->__unserialize(unserialize($serialized, ['allowed_classes' => $this->getUnserializeAllowedClasses()])); + } + + /** + * @return array|bool + */ + protected function getUnserializeAllowedClasses() + { + return false; + } +} diff --git a/system/src/Grav/Framework/ContentBlock/ContentBlock.php b/system/src/Grav/Framework/ContentBlock/ContentBlock.php new file mode 100644 index 0000000..efcd5f3 --- /dev/null +++ b/system/src/Grav/Framework/ContentBlock/ContentBlock.php @@ -0,0 +1,302 @@ +setContent('my inner content'); + * $outerBlock = ContentBlock::create(); + * $outerBlock->setContent(sprintf('Inside my outer block I have %s.', $innerBlock->getToken())); + * $outerBlock->addBlock($innerBlock); + * echo $outerBlock; + * + * @package Grav\Framework\ContentBlock + */ +class ContentBlock implements ContentBlockInterface +{ + use Serializable; + + /** @var int */ + protected $version = 1; + /** @var string */ + protected $id; + /** @var string */ + protected $tokenTemplate = '@@BLOCK-%s@@'; + /** @var string */ + protected $content = ''; + /** @var array */ + protected $blocks = []; + /** @var string */ + protected $checksum; + /** @var bool */ + protected $cached = true; + + /** + * @param string|null $id + * @return static + */ + public static function create($id = null) + { + return new static($id); + } + + /** + * @param array $serialized + * @return ContentBlockInterface + * @throws InvalidArgumentException + */ + public static function fromArray(array $serialized) + { + try { + $type = $serialized['_type'] ?? null; + $id = $serialized['id'] ?? null; + + if (!$type || !$id || !is_a($type, ContentBlockInterface::class, true)) { + throw new InvalidArgumentException('Bad data'); + } + + /** @var ContentBlockInterface $instance */ + $instance = new $type($id); + $instance->build($serialized); + } catch (Exception $e) { + throw new InvalidArgumentException(sprintf('Cannot unserialize Block: %s', $e->getMessage()), $e->getCode(), $e); + } + + return $instance; + } + + /** + * Block constructor. + * + * @param string|null $id + */ + public function __construct($id = null) + { + $this->id = $id ? (string) $id : $this->generateId(); + } + + /** + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getToken() + { + return sprintf($this->tokenTemplate, $this->getId()); + } + + /** + * @return array + */ + public function toArray() + { + $blocks = []; + /** @var ContentBlockInterface $block */ + foreach ($this->blocks as $block) { + $blocks[$block->getId()] = $block->toArray(); + } + + $array = [ + '_type' => get_class($this), + '_version' => $this->version, + 'id' => $this->id, + 'cached' => $this->cached + ]; + + if ($this->checksum) { + $array['checksum'] = $this->checksum; + } + + if ($this->content) { + $array['content'] = $this->content; + } + + if ($blocks) { + $array['blocks'] = $blocks; + } + + return $array; + } + + /** + * @return string + */ + public function toString() + { + if (!$this->blocks) { + return (string) $this->content; + } + + $tokens = []; + $replacements = []; + foreach ($this->blocks as $block) { + $tokens[] = $block->getToken(); + $replacements[] = $block->toString(); + } + + return str_replace($tokens, $replacements, (string) $this->content); + } + + /** + * @return string + */ + public function __toString() + { + try { + return $this->toString(); + } catch (Exception $e) { + return sprintf('Error while rendering block: %s', $e->getMessage()); + } + } + + /** + * @param array $serialized + * @return void + * @throws RuntimeException + */ + public function build(array $serialized) + { + $this->checkVersion($serialized); + + $this->id = $serialized['id'] ?? $this->generateId(); + $this->checksum = $serialized['checksum'] ?? null; + $this->cached = $serialized['cached'] ?? null; + + if (isset($serialized['content'])) { + $this->setContent($serialized['content']); + } + + $blocks = isset($serialized['blocks']) ? (array) $serialized['blocks'] : []; + foreach ($blocks as $block) { + $this->addBlock(self::fromArray($block)); + } + } + + /** + * @return bool + */ + public function isCached() + { + if (!$this->cached) { + return false; + } + + foreach ($this->blocks as $block) { + if (!$block->isCached()) { + return false; + } + } + + return true; + } + + /** + * @return $this + */ + public function disableCache() + { + $this->cached = false; + + return $this; + } + + /** + * @param string $checksum + * @return $this + */ + public function setChecksum($checksum) + { + $this->checksum = $checksum; + + return $this; + } + + /** + * @return string + */ + public function getChecksum() + { + return $this->checksum; + } + + /** + * @param string $content + * @return $this + */ + public function setContent($content) + { + $this->content = $content; + + return $this; + } + + /** + * @param ContentBlockInterface $block + * @return $this + */ + public function addBlock(ContentBlockInterface $block) + { + $this->blocks[$block->getId()] = $block; + + return $this; + } + + /** + * @return array + */ + final public function __serialize(): array + { + return $this->toArray(); + } + + /** + * @param array $data + * @return void + */ + final public function __unserialize(array $data): void + { + $this->build($data); + } + + /** + * @return string + */ + protected function generateId() + { + return uniqid('', true); + } + + /** + * @param array $serialized + * @return void + * @throws RuntimeException + */ + protected function checkVersion(array $serialized) + { + $version = isset($serialized['_version']) ? (int) $serialized['_version'] : 1; + if ($version !== $this->version) { + throw new RuntimeException(sprintf('Unsupported version %s', $version)); + } + } +} diff --git a/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php b/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php new file mode 100644 index 0000000..fb445a3 --- /dev/null +++ b/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php @@ -0,0 +1,90 @@ +getAssetsFast(); + + $this->sortAssets($assets['styles']); + $this->sortAssets($assets['scripts']); + $this->sortAssets($assets['html']); + + return $assets; + } + + /** + * @return array + */ + public function getFrameworks() + { + $assets = $this->getAssetsFast(); + + return array_keys($assets['frameworks']); + } + + /** + * @param string $location + * @return array + */ + public function getStyles($location = 'head') + { + return $this->getAssetsInLocation('styles', $location); + } + + /** + * @param string $location + * @return array + */ + public function getScripts($location = 'head') + { + return $this->getAssetsInLocation('scripts', $location); + } + + /** + * @param string $location + * @return array + */ + public function getHtml($location = 'bottom') + { + return $this->getAssetsInLocation('html', $location); + } + + /** + * @return array + */ + public function toArray() + { + $array = parent::toArray(); + + if ($this->frameworks) { + $array['frameworks'] = $this->frameworks; + } + if ($this->styles) { + $array['styles'] = $this->styles; + } + if ($this->scripts) { + $array['scripts'] = $this->scripts; + } + if ($this->html) { + $array['html'] = $this->html; + } + + return $array; + } + + /** + * @param array $serialized + * @return void + * @throws RuntimeException + */ + public function build(array $serialized) + { + parent::build($serialized); + + $this->frameworks = isset($serialized['frameworks']) ? (array) $serialized['frameworks'] : []; + $this->styles = isset($serialized['styles']) ? (array) $serialized['styles'] : []; + $this->scripts = isset($serialized['scripts']) ? (array) $serialized['scripts'] : []; + $this->html = isset($serialized['html']) ? (array) $serialized['html'] : []; + } + + /** + * @param string $framework + * @return $this + */ + public function addFramework($framework) + { + $this->frameworks[$framework] = 1; + + return $this; + } + + /** + * @param string|array $element + * @param int $priority + * @param string $location + * @return bool + * + * @example $block->addStyle('assets/js/my.js'); + * @example $block->addStyle(['href' => 'assets/js/my.js', 'media' => 'screen']); + */ + public function addStyle($element, $priority = 0, $location = 'head') + { + if (!is_array($element)) { + $element = ['href' => (string) $element]; + } + if (empty($element['href'])) { + return false; + } + if (!isset($this->styles[$location])) { + $this->styles[$location] = []; + } + + $id = !empty($element['id']) ? ['id' => (string) $element['id']] : []; + $href = $element['href']; + $type = !empty($element['type']) ? (string) $element['type'] : 'text/css'; + $media = !empty($element['media']) ? (string) $element['media'] : null; + unset( + $element['tag'], + $element['id'], + $element['rel'], + $element['content'], + $element['href'], + $element['type'], + $element['media'] + ); + + $this->styles[$location][md5($href) . sha1($href)] = [ + ':type' => 'file', + ':priority' => (int) $priority, + 'href' => $href, + 'type' => $type, + 'media' => $media, + 'element' => $element + ] + $id; + + return true; + } + + /** + * @param string|array $element + * @param int $priority + * @param string $location + * @return bool + */ + public function addInlineStyle($element, $priority = 0, $location = 'head') + { + if (!is_array($element)) { + $element = ['content' => (string) $element]; + } + if (empty($element['content'])) { + return false; + } + if (!isset($this->styles[$location])) { + $this->styles[$location] = []; + } + + $content = (string) $element['content']; + $type = !empty($element['type']) ? (string) $element['type'] : 'text/css'; + + $this->styles[$location][md5($content) . sha1($content)] = [ + ':type' => 'inline', + ':priority' => (int) $priority, + 'content' => $content, + 'type' => $type + ]; + + return true; + } + + /** + * @param string|array $element + * @param int $priority + * @param string $location + * @return bool + */ + public function addScript($element, $priority = 0, $location = 'head') + { + if (!is_array($element)) { + $element = ['src' => (string) $element]; + } + if (empty($element['src'])) { + return false; + } + if (!isset($this->scripts[$location])) { + $this->scripts[$location] = []; + } + + $src = $element['src']; + $type = !empty($element['type']) ? (string) $element['type'] : 'text/javascript'; + $defer = isset($element['defer']); + $async = isset($element['async']); + $handle = !empty($element['handle']) ? (string) $element['handle'] : ''; + + $this->scripts[$location][md5($src) . sha1($src)] = [ + ':type' => 'file', + ':priority' => (int) $priority, + 'src' => $src, + 'type' => $type, + 'defer' => $defer, + 'async' => $async, + 'handle' => $handle + ]; + + return true; + } + + /** + * @param string|array $element + * @param int $priority + * @param string $location + * @return bool + */ + public function addInlineScript($element, $priority = 0, $location = 'head') + { + if (!is_array($element)) { + $element = ['content' => (string) $element]; + } + if (empty($element['content'])) { + return false; + } + if (!isset($this->scripts[$location])) { + $this->scripts[$location] = []; + } + + $content = (string) $element['content']; + $type = !empty($element['type']) ? (string) $element['type'] : 'text/javascript'; + + $this->scripts[$location][md5($content) . sha1($content)] = [ + ':type' => 'inline', + ':priority' => (int) $priority, + 'content' => $content, + 'type' => $type + ]; + + return true; + } + + /** + * @param string $html + * @param int $priority + * @param string $location + * @return bool + */ + public function addHtml($html, $priority = 0, $location = 'bottom') + { + if (empty($html) || !is_string($html)) { + return false; + } + if (!isset($this->html[$location])) { + $this->html[$location] = []; + } + + $this->html[$location][md5($html) . sha1($html)] = [ + ':priority' => (int) $priority, + 'html' => $html + ]; + + return true; + } + + /** + * @return array + */ + protected function getAssetsFast() + { + $assets = [ + 'frameworks' => $this->frameworks, + 'styles' => $this->styles, + 'scripts' => $this->scripts, + 'html' => $this->html + ]; + + foreach ($this->blocks as $block) { + if ($block instanceof self) { + $blockAssets = $block->getAssetsFast(); + $assets['frameworks'] += $blockAssets['frameworks']; + + foreach ($blockAssets['styles'] as $location => $styles) { + if (!isset($assets['styles'][$location])) { + $assets['styles'][$location] = $styles; + } elseif ($styles) { + $assets['styles'][$location] += $styles; + } + } + + foreach ($blockAssets['scripts'] as $location => $scripts) { + if (!isset($assets['scripts'][$location])) { + $assets['scripts'][$location] = $scripts; + } elseif ($scripts) { + $assets['scripts'][$location] += $scripts; + } + } + + foreach ($blockAssets['html'] as $location => $htmls) { + if (!isset($assets['html'][$location])) { + $assets['html'][$location] = $htmls; + } elseif ($htmls) { + $assets['html'][$location] += $htmls; + } + } + } + } + + return $assets; + } + + /** + * @param string $type + * @param string $location + * @return array + */ + protected function getAssetsInLocation($type, $location) + { + $assets = $this->getAssetsFast(); + + if (empty($assets[$type][$location])) { + return []; + } + + $styles = $assets[$type][$location]; + $this->sortAssetsInLocation($styles); + + return $styles; + } + + /** + * @param array $items + * @return void + */ + protected function sortAssetsInLocation(array &$items) + { + $count = 0; + foreach ($items as &$item) { + $item[':order'] = ++$count; + } + unset($item); + + uasort( + $items, + static function ($a, $b) { + return $a[':priority'] <=> $b[':priority'] ?: $a[':order'] <=> $b[':order']; + } + ); + } + + /** + * @param array $array + * @return void + */ + protected function sortAssets(array &$array) + { + foreach ($array as $location => &$items) { + $this->sortAssetsInLocation($items); + } + } +} diff --git a/system/src/Grav/Framework/ContentBlock/HtmlBlockInterface.php b/system/src/Grav/Framework/ContentBlock/HtmlBlockInterface.php new file mode 100644 index 0000000..616e4a2 --- /dev/null +++ b/system/src/Grav/Framework/ContentBlock/HtmlBlockInterface.php @@ -0,0 +1,95 @@ +addStyle('assets/js/my.js'); + * @example $block->addStyle(['href' => 'assets/js/my.js', 'media' => 'screen']); + */ + public function addStyle($element, $priority = 0, $location = 'head'); + + /** + * @param string|array $element + * @param int $priority + * @param string $location + * @return bool + */ + public function addInlineStyle($element, $priority = 0, $location = 'head'); + + /** + * @param string|array $element + * @param int $priority + * @param string $location + * @return bool + */ + public function addScript($element, $priority = 0, $location = 'head'); + + + /** + * @param string|array $element + * @param int $priority + * @param string $location + * @return bool + */ + public function addInlineScript($element, $priority = 0, $location = 'head'); + + /** + * @param string $html + * @param int $priority + * @param string $location + * @return bool + */ + public function addHtml($html, $priority = 0, $location = 'bottom'); +} diff --git a/system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php b/system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php new file mode 100644 index 0000000..afa08aa --- /dev/null +++ b/system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php @@ -0,0 +1,306 @@ + 599) { + $code = 500; + } + $headers = $headers ?? []; + + return new Response($code, $headers, $content); + } + + /** + * @param array $content + * @param int|null $code + * @param array|null $headers + * @return Response + */ + protected function createJsonResponse(array $content, int $code = null, array $headers = null): ResponseInterface + { + $code = $code ?? $content['code'] ?? 200; + if (null === $code || $code < 100 || $code > 599) { + $code = 200; + } + $headers = ($headers ?? []) + [ + 'Content-Type' => 'application/json', + 'Cache-Control' => 'no-store, max-age=0' + ]; + + return new Response($code, $headers, json_encode($content)); + } + + /** + * @param string $filename + * @param string|resource|StreamInterface $resource + * @param array|null $headers + * @param array|null $options + * @return ResponseInterface + */ + protected function createDownloadResponse(string $filename, $resource, array $headers = null, array $options = null): ResponseInterface + { + // Required for IE, otherwise Content-Disposition may be ignored + if (ini_get('zlib.output_compression')) { + @ini_set('zlib.output_compression', 'Off'); + } + + $headers = $headers ?? []; + $options = $options ?? ['force_download' => true]; + + $file_parts = pathinfo($filename); + + if (!isset($headers['Content-Type'])) { + $mimetype = Utils::getMimeByExtension($file_parts['extension']); + + $headers['Content-Type'] = $mimetype; + } + + // TODO: add multipart download support. + //$headers['Accept-Ranges'] = 'bytes'; + + if (!empty($options['force_download'])) { + $headers['Content-Disposition'] = 'attachment; filename="' . $file_parts['basename'] . '"'; + } + + if (!isset($headers['Content-Length'])) { + $realpath = realpath($filename); + if ($realpath) { + $headers['Content-Length'] = filesize($realpath); + } + } + + $headers += [ + 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT', + 'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT', + 'Cache-Control' => 'no-store, no-cache, must-revalidate', + 'Pragma' => 'no-cache' + ]; + + return new Response(200, $headers, $resource); + } + + /** + * @param string $url + * @param int|null $code + * @return Response + */ + protected function createRedirectResponse(string $url, int $code = null): ResponseInterface + { + if (null === $code || $code < 301 || $code > 307) { + $code = (int)$this->getConfig()->get('system.pages.redirect_default_code', 302); + } + + $accept = $this->getAccept(['application/json', 'text/html']); + + if ($accept === 'application/json') { + return $this->createJsonResponse(['code' => $code, 'status' => 'redirect', 'redirect' => $url]); + } + + return new Response($code, ['Location' => $url]); + } + + /** + * @param Throwable $e + * @return ResponseInterface + */ + protected function createErrorResponse(Throwable $e): ResponseInterface + { + $response = $this->getErrorJson($e); + $message = $response['message']; + $code = $response['code']; + $reason = $e instanceof RequestException ? $e->getHttpReason() : null; + $accept = $this->getAccept(['application/json', 'text/html']); + + $request = $this->getRequest(); + $context = $request->getAttributes(); + + /** @var Route $route */ + $route = $context['route'] ?? null; + + $ext = $route ? $route->getExtension() : null; + if ($ext !== 'json' && $accept === 'text/html') { + $method = $request->getMethod(); + + // On POST etc, redirect back to the previous page. + if ($method !== 'GET' && $method !== 'HEAD') { + $this->setMessage($message, 'error'); + $referer = $request->getHeaderLine('Referer'); + + return $this->createRedirectResponse($referer, 303); + } + + // TODO: improve error page + return $this->createHtmlResponse($response['message'], $code); + } + + return new Response($code, ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason); + } + + /** + * @param Throwable $e + * @return ResponseInterface + */ + protected function createJsonErrorResponse(Throwable $e): ResponseInterface + { + $response = $this->getErrorJson($e); + $reason = $e instanceof RequestException ? $e->getHttpReason() : null; + + return new Response($response['code'], ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason); + } + + /** + * @param Throwable $e + * @return array + */ + protected function getErrorJson(Throwable $e): array + { + $code = $this->getErrorCode($e instanceof RequestException ? $e->getHttpCode() : $e->getCode()); + if ($e instanceof ValidationException) { + $message = $e->getMessage(); + } else { + $message = htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } + + $extra = $e instanceof JsonSerializable ? $e->jsonSerialize() : []; + + $response = [ + 'code' => $code, + 'status' => 'error', + 'message' => $message, + 'error' => [ + 'code' => $code, + 'message' => $message + ] + $extra + ]; + + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + if ($debugger->enabled()) { + $response['error'] += [ + 'type' => get_class($e), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => explode("\n", $e->getTraceAsString()) + ]; + } + + return $response; + } + + /** + * @param int $code + * @return int + */ + protected function getErrorCode(int $code): int + { + static $errorCodes = [ + 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, + 422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 511 + ]; + + if (!in_array($code, $errorCodes, true)) { + $code = 500; + } + + return $code; + } + + /** + * @param array $compare + * @return mixed + */ + protected function getAccept(array $compare) + { + $accepted = []; + foreach ($this->getRequest()->getHeader('Accept') as $accept) { + foreach (explode(',', $accept) as $item) { + if (!$item) { + continue; + } + + $split = explode(';q=', $item); + $mime = array_shift($split); + $priority = array_shift($split) ?? 1.0; + + $accepted[$mime] = $priority; + } + } + + arsort($accepted); + + // TODO: add support for image/* etc + $list = array_intersect($compare, array_keys($accepted)); + if (!$list && (isset($accepted['*/*']) || isset($accepted['*']))) { + return reset($compare); + } + + return reset($list); + } + + /** + * @return ServerRequestInterface + */ + abstract protected function getRequest(): ServerRequestInterface; + + /** + * @param string $message + * @param string $type + * @return $this + */ + abstract protected function setMessage(string $message, string $type = 'info'); + + /** + * @return Config + */ + abstract protected function getConfig(): Config; +} diff --git a/system/src/Grav/Framework/DI/Container.php b/system/src/Grav/Framework/DI/Container.php new file mode 100644 index 0000000..76434a3 --- /dev/null +++ b/system/src/Grav/Framework/DI/Container.php @@ -0,0 +1,35 @@ +offsetGet($id); + } + + /** + * @param string $id + * @return bool + */ + public function has($id): bool + { + return $this->offsetExists($id); + } +} diff --git a/system/src/Grav/Framework/File/AbstractFile.php b/system/src/Grav/Framework/File/AbstractFile.php new file mode 100644 index 0000000..b82bb33 --- /dev/null +++ b/system/src/Grav/Framework/File/AbstractFile.php @@ -0,0 +1,441 @@ +filesystem = $filesystem ?? Filesystem::getInstance(); + $this->setFilepath($filepath); + } + + /** + * Unlock file when the object gets destroyed. + */ + public function __destruct() + { + if ($this->isLocked()) { + $this->unlock(); + } + } + + /** + * @return void + */ + public function __clone() + { + $this->handle = null; + $this->locked = false; + } + + /** + * @return array + */ + final public function __serialize(): array + { + return ['filesystem_normalize' => $this->filesystem->getNormalization()] + $this->doSerialize(); + } + + /** + * @param array $data + * @return void + */ + final public function __unserialize(array $data): void + { + $this->filesystem = Filesystem::getInstance($data['filesystem_normalize'] ?? null); + + $this->doUnserialize($data); + } + + /** + * {@inheritdoc} + * @see FileInterface::getFilePath() + */ + public function getFilePath(): string + { + return $this->filepath; + } + + /** + * {@inheritdoc} + * @see FileInterface::getPath() + */ + public function getPath(): string + { + if (null === $this->path) { + $this->setPathInfo(); + } + + return $this->path ?? ''; + } + + /** + * {@inheritdoc} + * @see FileInterface::getFilename() + */ + public function getFilename(): string + { + if (null === $this->filename) { + $this->setPathInfo(); + } + + return $this->filename ?? ''; + } + + /** + * {@inheritdoc} + * @see FileInterface::getBasename() + */ + public function getBasename(): string + { + if (null === $this->basename) { + $this->setPathInfo(); + } + + return $this->basename ?? ''; + } + + /** + * {@inheritdoc} + * @see FileInterface::getExtension() + */ + public function getExtension(bool $withDot = false): string + { + if (null === $this->extension) { + $this->setPathInfo(); + } + + return ($withDot ? '.' : '') . $this->extension; + } + + /** + * {@inheritdoc} + * @see FileInterface::exists() + */ + public function exists(): bool + { + return is_file($this->filepath); + } + + /** + * {@inheritdoc} + * @see FileInterface::getCreationTime() + */ + public function getCreationTime(): int + { + return is_file($this->filepath) ? (int)filectime($this->filepath) : time(); + } + + /** + * {@inheritdoc} + * @see FileInterface::getModificationTime() + */ + public function getModificationTime(): int + { + return is_file($this->filepath) ? (int)filemtime($this->filepath) : time(); + } + + /** + * {@inheritdoc} + * @see FileInterface::lock() + */ + public function lock(bool $block = true): bool + { + if (!$this->handle) { + if (!$this->mkdir($this->getPath())) { + throw new RuntimeException('Creating directory failed for ' . $this->filepath); + } + $this->handle = @fopen($this->filepath, 'cb+') ?: null; + if (!$this->handle) { + $error = error_get_last(); + + throw new RuntimeException("Opening file for writing failed on error {$error['message']}"); + } + } + + $lock = $block ? LOCK_EX : LOCK_EX | LOCK_NB; + + // Some filesystems do not support file locks, only fail if another process holds the lock. + $this->locked = flock($this->handle, $lock, $wouldblock) || !$wouldblock; + + return $this->locked; + } + + /** + * {@inheritdoc} + * @see FileInterface::unlock() + */ + public function unlock(): bool + { + if (!$this->handle) { + return false; + } + + if ($this->locked) { + flock($this->handle, LOCK_UN | LOCK_NB); + $this->locked = false; + } + + fclose($this->handle); + $this->handle = null; + + return true; + } + + /** + * {@inheritdoc} + * @see FileInterface::isLocked() + */ + public function isLocked(): bool + { + return $this->locked; + } + + /** + * {@inheritdoc} + * @see FileInterface::isReadable() + */ + public function isReadable(): bool + { + return is_readable($this->filepath) && is_file($this->filepath); + } + + /** + * {@inheritdoc} + * @see FileInterface::isWritable() + */ + public function isWritable(): bool + { + if (!file_exists($this->filepath)) { + return $this->isWritablePath($this->getPath()); + } + + return is_writable($this->filepath) && is_file($this->filepath); + } + + /** + * {@inheritdoc} + * @see FileInterface::load() + */ + public function load() + { + return file_get_contents($this->filepath); + } + + /** + * {@inheritdoc} + * @see FileInterface::save() + */ + public function save($data): void + { + $filepath = $this->filepath; + $dir = $this->getPath(); + + if (!$this->mkdir($dir)) { + throw new RuntimeException('Creating directory failed for ' . $filepath); + } + + try { + if ($this->handle) { + $tmp = true; + // As we are using non-truncating locking, make sure that the file is empty before writing. + if (@ftruncate($this->handle, 0) === false || @fwrite($this->handle, $data) === false) { + // Writing file failed, throw an error. + $tmp = false; + } + } else { + // Support for symlinks. + $realpath = is_link($filepath) ? realpath($filepath) : $filepath; + if ($realpath === false) { + throw new RuntimeException('Failed to save file ' . $filepath); + } + + // Create file with a temporary name and rename it to make the save action atomic. + $tmp = $this->tempname($realpath); + if (@file_put_contents($tmp, $data) === false) { + $tmp = false; + } elseif (@rename($tmp, $realpath) === false) { + @unlink($tmp); + $tmp = false; + } + } + } catch (Exception $e) { + $tmp = false; + } + + if ($tmp === false) { + throw new RuntimeException('Failed to save file ' . $filepath); + } + + // Touch the directory as well, thus marking it modified. + @touch($dir); + } + + /** + * {@inheritdoc} + * @see FileInterface::rename() + */ + public function rename(string $path): bool + { + if ($this->exists() && !@rename($this->filepath, $path)) { + return false; + } + + $this->setFilepath($path); + + return true; + } + + /** + * {@inheritdoc} + * @see FileInterface::delete() + */ + public function delete(): bool + { + return @unlink($this->filepath); + } + + /** + * @param string $dir + * @return bool + * @throws RuntimeException + * @internal + */ + protected function mkdir(string $dir): bool + { + // Silence error for open_basedir; should fail in mkdir instead. + if (@is_dir($dir)) { + return true; + } + + $success = @mkdir($dir, 0777, true); + + if (!$success) { + // Take yet another look, make sure that the folder doesn't exist. + clearstatcache(true, $dir); + if (!@is_dir($dir)) { + return false; + } + } + + return true; + } + + /** + * @return array + */ + protected function doSerialize(): array + { + return [ + 'filepath' => $this->filepath + ]; + } + + /** + * @param array $serialized + * @return void + */ + protected function doUnserialize(array $serialized): void + { + $this->setFilepath($serialized['filepath']); + } + + /** + * @param string $filepath + */ + protected function setFilepath(string $filepath): void + { + $this->filepath = $filepath; + $this->filename = null; + $this->basename = null; + $this->path = null; + $this->extension = null; + } + + protected function setPathInfo(): void + { + /** @var array $pathInfo */ + $pathInfo = $this->filesystem->pathinfo($this->filepath); + + $this->filename = $pathInfo['filename'] ?? null; + $this->basename = $pathInfo['basename'] ?? null; + $this->path = $pathInfo['dirname'] ?? null; + $this->extension = $pathInfo['extension'] ?? null; + } + + /** + * @param string $dir + * @return bool + * @internal + */ + protected function isWritablePath(string $dir): bool + { + if ($dir === '') { + return false; + } + + if (!file_exists($dir)) { + // Recursively look up in the directory tree. + return $this->isWritablePath($this->filesystem->parent($dir)); + } + + return is_dir($dir) && is_writable($dir); + } + + /** + * @param string $filename + * @param int $length + * @return string + */ + protected function tempname(string $filename, int $length = 5) + { + do { + $test = $filename . substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length); + } while (file_exists($test)); + + return $test; + } +} diff --git a/system/src/Grav/Framework/File/CsvFile.php b/system/src/Grav/Framework/File/CsvFile.php new file mode 100644 index 0000000..999f88a --- /dev/null +++ b/system/src/Grav/Framework/File/CsvFile.php @@ -0,0 +1,31 @@ +formatter = $formatter; + } + + /** + * {@inheritdoc} + * @see FileInterface::load() + */ + public function load() + { + $raw = parent::load(); + + try { + if (!is_string($raw)) { + throw new RuntimeException('Bad Data'); + } + + return $this->formatter->decode($raw); + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf("Failed to load file '%s': %s", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + * @see FileInterface::save() + */ + public function save($data): void + { + if (is_string($data)) { + // Make sure that the string is valid data. + try { + $this->formatter->decode($data); + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf("Failed to save file '%s': %s", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e); + } + $encoded = $data; + } else { + $encoded = $this->formatter->encode($data); + } + + parent::save($encoded); + } +} diff --git a/system/src/Grav/Framework/File/File.php b/system/src/Grav/Framework/File/File.php new file mode 100644 index 0000000..1d4055a --- /dev/null +++ b/system/src/Grav/Framework/File/File.php @@ -0,0 +1,44 @@ +config = $config; + } + + /** + * @return string + */ + public function getMimeType(): string + { + $mime = $this->getConfig('mime'); + + return is_string($mime) ? $mime : 'application/octet-stream'; + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::getDefaultFileExtension() + */ + public function getDefaultFileExtension(): string + { + $extensions = $this->getSupportedFileExtensions(); + + // Call fails on bad configuration. + return reset($extensions) ?: ''; + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::getSupportedFileExtensions() + */ + public function getSupportedFileExtensions(): array + { + $extensions = $this->getConfig('file_extension'); + + // Call fails on bad configuration. + return is_string($extensions) ? [$extensions] : $extensions; + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::encode() + */ + abstract public function encode($data): string; + + /** + * {@inheritdoc} + * @see FileFormatterInterface::decode() + */ + abstract public function decode($data); + + + /** + * @return array + */ + public function __serialize(): array + { + return ['config' => $this->config]; + } + + /** + * @param array $data + * @return void + */ + public function __unserialize(array $data): void + { + $this->config = $data['config']; + } + + /** + * Get either full configuration or a single option. + * + * @param string|null $name Configuration option (optional) + * @return mixed + */ + protected function getConfig(string $name = null) + { + if (null !== $name) { + return $this->config[$name] ?? null; + } + + return $this->config; + } +} diff --git a/system/src/Grav/Framework/File/Formatter/CsvFormatter.php b/system/src/Grav/Framework/File/Formatter/CsvFormatter.php new file mode 100644 index 0000000..9d0a9a8 --- /dev/null +++ b/system/src/Grav/Framework/File/Formatter/CsvFormatter.php @@ -0,0 +1,169 @@ + ['.csv', '.tsv'], + 'delimiter' => ',', + 'mime' => 'text/x-csv' + ]; + + parent::__construct($config); + } + + /** + * Returns delimiter used to both encode and decode CSV. + * + * @return string + */ + public function getDelimiter(): string + { + // Call fails on bad configuration. + return $this->getConfig('delimiter'); + } + + /** + * @param array $data + * @param string|null $delimiter + * @return string + * @see FileFormatterInterface::encode() + */ + public function encode($data, $delimiter = null): string + { + if (count($data) === 0) { + return ''; + } + $delimiter = $delimiter ?? $this->getDelimiter(); + $header = array_keys(reset($data)); + + // Encode the field names + $string = $this->encodeLine($header, $delimiter); + + // Encode the data + foreach ($data as $row) { + $string .= $this->encodeLine($row, $delimiter); + } + + return $string; + } + + /** + * @param string $data + * @param string|null $delimiter + * @return array + * @see FileFormatterInterface::decode() + */ + public function decode($data, $delimiter = null): array + { + $delimiter = $delimiter ?? $this->getDelimiter(); + $lines = preg_split('/\r\n|\r|\n/', $data); + if ($lines === false) { + throw new RuntimeException('Decoding CSV failed'); + } + + // Get the field names + $headerStr = array_shift($lines); + if (!$headerStr) { + throw new RuntimeException('CSV header missing'); + } + + $header = str_getcsv($headerStr, $delimiter); + + // Allow for replacing a null string with null/empty value + $null_replace = $this->getConfig('null'); + + // Get the data + $list = []; + $line = null; + try { + foreach ($lines as $line) { + if (!empty($line)) { + $csv_line = str_getcsv($line, $delimiter); + + if ($null_replace) { + array_walk($csv_line, static function (&$el) use ($null_replace) { + $el = str_replace($null_replace, "\0", $el); + }); + } + + $list[] = array_combine($header, $csv_line); + } + } + } catch (Exception $e) { + throw new RuntimeException('Badly formatted CSV line: ' . $line); + } + + return $list; + } + + /** + * @param array $line + * @param string $delimiter + * @return string + */ + protected function encodeLine(array $line, string $delimiter): string + { + foreach ($line as $key => &$value) { + // Oops, we need to convert the line to a string. + if (!is_scalar($value)) { + if (is_array($value) || $value instanceof JsonSerializable || $value instanceof stdClass) { + $value = json_encode($value); + } elseif (is_object($value)) { + if (method_exists($value, 'toJson')) { + $value = $value->toJson(); + } elseif (method_exists($value, 'toArray')) { + $value = json_encode($value->toArray()); + } + } + } + + $value = $this->escape((string)$value); + } + unset($value); + + return implode($delimiter, $line). "\n"; + } + + /** + * @param string $value + * @return string + */ + protected function escape(string $value) + { + if (preg_match('/[,"\r\n]/u', $value)) { + $value = '"' . preg_replace('/"/', '""', $value) . '"'; + } + + return $value; + } +} diff --git a/system/src/Grav/Framework/File/Formatter/FormatterInterface.php b/system/src/Grav/Framework/File/Formatter/FormatterInterface.php new file mode 100644 index 0000000..757e229 --- /dev/null +++ b/system/src/Grav/Framework/File/Formatter/FormatterInterface.php @@ -0,0 +1,12 @@ + '.ini' + ]; + + parent::__construct($config); + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::encode() + */ + public function encode($data): string + { + $string = ''; + foreach ($data as $key => $value) { + $string .= $key . '="' . preg_replace( + ['/"/', '/\\\/', "/\t/", "/\n/", "/\r/"], + ['\"', '\\\\', '\t', '\n', '\r'], + $value + ) . "\"\n"; + } + + return $string; + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::decode() + */ + public function decode($data): array + { + $decoded = @parse_ini_string($data); + + if ($decoded === false) { + throw new \RuntimeException('Decoding INI failed'); + } + + return $decoded; + } +} diff --git a/system/src/Grav/Framework/File/Formatter/JsonFormatter.php b/system/src/Grav/Framework/File/Formatter/JsonFormatter.php new file mode 100644 index 0000000..6b19690 --- /dev/null +++ b/system/src/Grav/Framework/File/Formatter/JsonFormatter.php @@ -0,0 +1,165 @@ + JSON_FORCE_OBJECT, + 'JSON_HEX_QUOT' => JSON_HEX_QUOT, + 'JSON_HEX_TAG' => JSON_HEX_TAG, + 'JSON_HEX_AMP' => JSON_HEX_AMP, + 'JSON_HEX_APOS' => JSON_HEX_APOS, + 'JSON_INVALID_UTF8_IGNORE' => JSON_INVALID_UTF8_IGNORE, + 'JSON_INVALID_UTF8_SUBSTITUTE' => JSON_INVALID_UTF8_SUBSTITUTE, + 'JSON_NUMERIC_CHECK' => JSON_NUMERIC_CHECK, + 'JSON_PARTIAL_OUTPUT_ON_ERROR' => JSON_PARTIAL_OUTPUT_ON_ERROR, + 'JSON_PRESERVE_ZERO_FRACTION' => JSON_PRESERVE_ZERO_FRACTION, + 'JSON_PRETTY_PRINT' => JSON_PRETTY_PRINT, + 'JSON_UNESCAPED_LINE_TERMINATORS' => JSON_UNESCAPED_LINE_TERMINATORS, + 'JSON_UNESCAPED_SLASHES' => JSON_UNESCAPED_SLASHES, + 'JSON_UNESCAPED_UNICODE' => JSON_UNESCAPED_UNICODE, + //'JSON_THROW_ON_ERROR' => JSON_THROW_ON_ERROR // PHP 7.3 + ]; + + /** @var array */ + protected $decodeOptions = [ + 'JSON_BIGINT_AS_STRING' => JSON_BIGINT_AS_STRING, + 'JSON_INVALID_UTF8_IGNORE' => JSON_INVALID_UTF8_IGNORE, + 'JSON_INVALID_UTF8_SUBSTITUTE' => JSON_INVALID_UTF8_SUBSTITUTE, + 'JSON_OBJECT_AS_ARRAY' => JSON_OBJECT_AS_ARRAY, + //'JSON_THROW_ON_ERROR' => JSON_THROW_ON_ERROR // PHP 7.3 + ]; + + public function __construct(array $config = []) + { + $config += [ + 'file_extension' => '.json', + 'encode_options' => 0, + 'decode_assoc' => true, + 'decode_depth' => 512, + 'decode_options' => 0 + ]; + + parent::__construct($config); + } + + /** + * Returns options used in encode() function. + * + * @return int + */ + public function getEncodeOptions(): int + { + $options = $this->getConfig('encode_options'); + if (!is_int($options)) { + if (is_string($options)) { + $list = preg_split('/[\s,|]+/', $options); + $options = 0; + foreach ($list as $option) { + if (isset($this->encodeOptions[$option])) { + $options += $this->encodeOptions[$option]; + } + } + } else { + $options = 0; + } + } + + return $options; + } + + /** + * Returns options used in decode() function. + * + * @return int + */ + public function getDecodeOptions(): int + { + $options = $this->getConfig('decode_options'); + if (!is_int($options)) { + if (is_string($options)) { + $list = preg_split('/[\s,|]+/', $options); + $options = 0; + foreach ($list as $option) { + if (isset($this->decodeOptions[$option])) { + $options += $this->decodeOptions[$option]; + } + } + } else { + $options = 0; + } + } + + return $options; + } + + /** + * Returns recursion depth used in decode() function. + * + * @return int + */ + public function getDecodeDepth(): int + { + return $this->getConfig('decode_depth'); + } + + /** + * Returns true if JSON objects will be converted into associative arrays. + * + * @return bool + */ + public function getDecodeAssoc(): bool + { + return $this->getConfig('decode_assoc'); + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::encode() + */ + public function encode($data): string + { + $encoded = @json_encode($data, $this->getEncodeOptions()); + + if ($encoded === false && json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Encoding JSON failed: ' . json_last_error_msg()); + } + + return $encoded ?: ''; + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::decode() + */ + public function decode($data) + { + $decoded = @json_decode($data, $this->getDecodeAssoc(), $this->getDecodeDepth(), $this->getDecodeOptions()); + + if (null === $decoded && json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Decoding JSON failed: ' . json_last_error_msg()); + } + + return $decoded; + } +} diff --git a/system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php b/system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php new file mode 100644 index 0000000..8a624df --- /dev/null +++ b/system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php @@ -0,0 +1,160 @@ + '.md', + 'header' => 'header', + 'body' => 'markdown', + 'raw' => 'frontmatter', + 'yaml' => ['inline' => 20] + ]; + + parent::__construct($config); + + $this->headerFormatter = $headerFormatter ?? new YamlFormatter($config['yaml']); + } + + /** + * Returns header field used in both encode() and decode(). + * + * @return string + */ + public function getHeaderField(): string + { + return $this->getConfig('header'); + } + + /** + * Returns body field used in both encode() and decode(). + * + * @return string + */ + public function getBodyField(): string + { + return $this->getConfig('body'); + } + + /** + * Returns raw field used in both encode() and decode(). + * + * @return string + */ + public function getRawField(): string + { + return $this->getConfig('raw'); + } + + /** + * Returns header formatter object used in both encode() and decode(). + * + * @return FileFormatterInterface + */ + public function getHeaderFormatter(): FileFormatterInterface + { + return $this->headerFormatter; + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::encode() + */ + public function encode($data): string + { + $headerVar = $this->getHeaderField(); + $bodyVar = $this->getBodyField(); + + $header = isset($data[$headerVar]) ? (array) $data[$headerVar] : []; + $body = isset($data[$bodyVar]) ? (string) $data[$bodyVar] : ''; + + // Create Markdown file with YAML header. + $encoded = ''; + if ($header) { + $encoded = "---\n" . trim($this->getHeaderFormatter()->encode($data['header'])) . "\n---\n\n"; + } + $encoded .= $body; + + // Normalize line endings to Unix style. + $encoded = preg_replace("/(\r\n|\r)/u", "\n", $encoded); + if (null === $encoded) { + throw new \RuntimeException('Encoding markdown failed'); + } + + return $encoded; + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::decode() + */ + public function decode($data): array + { + $headerVar = $this->getHeaderField(); + $bodyVar = $this->getBodyField(); + $rawVar = $this->getRawField(); + + // Define empty content + $content = [ + $headerVar => [], + $bodyVar => '' + ]; + + $headerRegex = "/^---\n(.+?)\n---\n{0,}(.*)$/uis"; + + // Normalize line endings to Unix style. + $data = preg_replace("/(\r\n|\r)/u", "\n", $data); + if (null === $data) { + throw new \RuntimeException('Decoding markdown failed'); + } + + // Parse header. + preg_match($headerRegex, ltrim($data), $matches); + if (empty($matches)) { + $content[$bodyVar] = $data; + } else { + // Normalize frontmatter. + $frontmatter = preg_replace("/\n\t/", "\n ", $matches[1]); + if ($rawVar) { + $content[$rawVar] = $frontmatter; + } + $content[$headerVar] = $this->getHeaderFormatter()->decode($frontmatter); + $content[$bodyVar] = $matches[2]; + } + + return $content; + } + + public function __serialize(): array + { + return parent::__serialize() + ['headerFormatter' => $this->headerFormatter]; + } + + public function __unserialize(array $data): void + { + parent::__unserialize($data); + + $this->headerFormatter = $data['headerFormatter'] ?? new YamlFormatter(['inline' => 20]); + } +} diff --git a/system/src/Grav/Framework/File/Formatter/SerializeFormatter.php b/system/src/Grav/Framework/File/Formatter/SerializeFormatter.php new file mode 100644 index 0000000..f045d71 --- /dev/null +++ b/system/src/Grav/Framework/File/Formatter/SerializeFormatter.php @@ -0,0 +1,98 @@ + '.ser', + 'decode_options' => ['allowed_classes' => [stdClass::class]] + ]; + + parent::__construct($config); + } + + /** + * Returns options used in decode(). + * + * By default only allow stdClass class. + * + * @return array + */ + public function getOptions() + { + return $this->getConfig('decode_options'); + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::encode() + */ + public function encode($data): string + { + return serialize($this->preserveLines($data, ["\n", "\r"], ['\\n', '\\r'])); + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::decode() + */ + public function decode($data) + { + $classes = $this->getOptions()['allowed_classes'] ?? false; + $decoded = @unserialize($data, ['allowed_classes' => $classes]); + + if ($decoded === false && $data !== serialize(false)) { + throw new RuntimeException('Decoding serialized data failed'); + } + + return $this->preserveLines($decoded, ['\\n', '\\r'], ["\n", "\r"]); + } + + /** + * Preserve new lines, recursive function. + * + * @param mixed $data + * @param array $search + * @param array $replace + * @return mixed + */ + protected function preserveLines($data, array $search, array $replace) + { + if (is_string($data)) { + $data = str_replace($search, $replace, $data); + } elseif (is_array($data)) { + foreach ($data as &$value) { + $value = $this->preserveLines($value, $search, $replace); + } + unset($value); + } + + return $data; + } +} diff --git a/system/src/Grav/Framework/File/Formatter/YamlFormatter.php b/system/src/Grav/Framework/File/Formatter/YamlFormatter.php new file mode 100644 index 0000000..32d4d29 --- /dev/null +++ b/system/src/Grav/Framework/File/Formatter/YamlFormatter.php @@ -0,0 +1,127 @@ + '.yaml', + 'inline' => 5, + 'indent' => 2, + 'native' => true, + 'compat' => true + ]; + + parent::__construct($config); + } + + /** + * @return int + */ + public function getInlineOption(): int + { + return $this->getConfig('inline'); + } + + /** + * @return int + */ + public function getIndentOption(): int + { + return $this->getConfig('indent'); + } + + /** + * @return bool + */ + public function useNativeDecoder(): bool + { + return $this->getConfig('native'); + } + + /** + * @return bool + */ + public function useCompatibleDecoder(): bool + { + return $this->getConfig('compat'); + } + + /** + * @param array $data + * @param int|null $inline + * @param int|null $indent + * @return string + * @see FileFormatterInterface::encode() + */ + public function encode($data, $inline = null, $indent = null): string + { + try { + return YamlParser::dump( + $data, + $inline ? (int) $inline : $this->getInlineOption(), + $indent ? (int) $indent : $this->getIndentOption(), + YamlParser::DUMP_EXCEPTION_ON_INVALID_TYPE + ); + } catch (DumpException $e) { + throw new RuntimeException('Encoding YAML failed: ' . $e->getMessage(), 0, $e); + } + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::decode() + */ + public function decode($data): array + { + // Try native PECL YAML PHP extension first if available. + if (function_exists('yaml_parse') && $this->useNativeDecoder()) { + // Safely decode YAML. + $saved = @ini_get('yaml.decode_php'); + @ini_set('yaml.decode_php', '0'); + $decoded = @yaml_parse($data); + @ini_set('yaml.decode_php', $saved); + + if ($decoded !== false) { + return (array) $decoded; + } + } + + try { + return (array) YamlParser::parse($data); + } catch (ParseException $e) { + if ($this->useCompatibleDecoder()) { + return (array) FallbackYamlParser::parse($data); + } + + throw new RuntimeException('Decoding YAML failed: ' . $e->getMessage(), 0, $e); + } + } +} diff --git a/system/src/Grav/Framework/File/IniFile.php b/system/src/Grav/Framework/File/IniFile.php new file mode 100644 index 0000000..6cda609 --- /dev/null +++ b/system/src/Grav/Framework/File/IniFile.php @@ -0,0 +1,31 @@ +setNormalization() + * @return Filesystem + */ + public static function getInstance(bool $normalize = null): Filesystem + { + if ($normalize === true) { + $instance = &static::$safe; + } elseif ($normalize === false) { + $instance = &static::$unsafe; + } else { + $instance = &static::$default; + } + + if (null === $instance) { + $instance = new static($normalize); + } + + return $instance; + } + + /** + * Always use Filesystem::getInstance() instead. + * + * @param bool|null $normalize + * @internal + */ + protected function __construct(bool $normalize = null) + { + $this->normalize = $normalize; + } + + /** + * Set path normalization. + * + * Default option enables normalization for the streams only, but you can force the normalization to be either + * on or off for every path. Disabling path normalization speeds up the calls, but may cause issues if paths were + * not normalized. + * + * @param bool|null $normalize + * @return Filesystem + */ + public function setNormalization(bool $normalize = null): self + { + return static::getInstance($normalize); + } + + /** + * @return bool|null + */ + public function getNormalization(): ?bool + { + return $this->normalize; + } + + /** + * Force all paths to be normalized. + * + * @return self + */ + public function unsafe(): self + { + return static::getInstance(true); + } + + /** + * Force all paths not to be normalized (speeds up the calls if given paths are known to be normalized). + * + * @return self + */ + public function safe(): self + { + return static::getInstance(false); + } + + /** + * {@inheritdoc} + * @see FilesystemInterface::parent() + */ + public function parent(string $path, int $levels = 1): string + { + [$scheme, $path] = $this->getSchemeAndHierarchy($path); + + if ($this->normalize !== false) { + $path = $this->normalizePathPart($path); + } + + if ($path === '' || $path === '.') { + return ''; + } + + [$scheme, $parent] = $this->dirnameInternal($scheme, $path, $levels); + + return $parent !== $path ? $this->toString($scheme, $parent) : ''; + } + + /** + * {@inheritdoc} + * @see FilesystemInterface::normalize() + */ + public function normalize(string $path): string + { + [$scheme, $path] = $this->getSchemeAndHierarchy($path); + + $path = $this->normalizePathPart($path); + + return $this->toString($scheme, $path); + } + + /** + * {@inheritdoc} + * @see FilesystemInterface::basename() + */ + public function basename(string $path, ?string $suffix = null): string + { + return $suffix ? basename($path, $suffix) : basename($path); + } + + /** + * {@inheritdoc} + * @see FilesystemInterface::dirname() + */ + public function dirname(string $path, int $levels = 1): string + { + [$scheme, $path] = $this->getSchemeAndHierarchy($path); + + if ($this->normalize || ($scheme && null === $this->normalize)) { + $path = $this->normalizePathPart($path); + } + + [$scheme, $path] = $this->dirnameInternal($scheme, $path, $levels); + + return $this->toString($scheme, $path); + } + + /** + * Gets full path with trailing slash. + * + * @param string $path + * @param int $levels + * @return string + */ + public function pathname(string $path, int $levels = 1): string + { + $path = $this->dirname($path, $levels); + + return $path !== '.' ? $path . '/' : ''; + } + + /** + * {@inheritdoc} + * @see FilesystemInterface::pathinfo() + */ + public function pathinfo(string $path, ?int $options = null) + { + [$scheme, $path] = $this->getSchemeAndHierarchy($path); + + if ($this->normalize || ($scheme && null === $this->normalize)) { + $path = $this->normalizePathPart($path); + } + + return $this->pathinfoInternal($scheme, $path, $options); + } + + /** + * @param string|null $scheme + * @param string $path + * @param int $levels + * @return array + */ + protected function dirnameInternal(?string $scheme, string $path, int $levels = 1): array + { + $path = dirname($path, $levels); + + if (null !== $scheme && $path === '.') { + return [$scheme, '']; + } + + // In Windows dirname() may return backslashes, fix that. + if (DIRECTORY_SEPARATOR !== '/') { + $path = str_replace('\\', '/', $path); + } + + return [$scheme, $path]; + } + + /** + * @param string|null $scheme + * @param string $path + * @param int|null $options + * @return array|string + */ + protected function pathinfoInternal(?string $scheme, string $path, ?int $options = null) + { + if ($options) { + return pathinfo($path, $options); + } + + $info = pathinfo($path); + + if (null !== $scheme) { + $info['scheme'] = $scheme; + $dirname = isset($info['dirname']) && $info['dirname'] !== '.' ? $info['dirname'] : null; + + if (null !== $dirname) { + // In Windows dirname may be using backslashes, fix that. + if (DIRECTORY_SEPARATOR !== '/') { + $dirname = str_replace('\\', '/', $dirname); + } + + $info['dirname'] = $scheme . '://' . $dirname; + } else { + $info = ['dirname' => $scheme . '://'] + $info; + } + } + + return $info; + } + + /** + * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> array(file, tmp)). + * + * @param string $filename + * @return array + */ + protected function getSchemeAndHierarchy(string $filename): array + { + $components = explode('://', $filename, 2); + + return 2 === count($components) ? $components : [null, $components[0]]; + } + + /** + * @param string|null $scheme + * @param string $path + * @return string + */ + protected function toString(?string $scheme, string $path): string + { + if ($scheme) { + return $scheme . '://' . $path; + } + + return $path; + } + + /** + * @param string $path + * @return string + * @throws RuntimeException + */ + protected function normalizePathPart(string $path): string + { + // Quick check for empty path. + if ($path === '' || $path === '.') { + return ''; + } + + // Quick check for root. + if ($path === '/') { + return '/'; + } + + // If the last character is not '/' or any of '\', './', '//' and '..' are not found, path is clean and we're done. + if ($path[-1] !== '/' && !preg_match('`(\\\\|\./|//|\.\.)`', $path)) { + return $path; + } + + // Convert backslashes + $path = strtr($path, ['\\' => '/']); + + $parts = explode('/', $path); + + // Keep absolute paths. + $root = ''; + if ($parts[0] === '') { + $root = '/'; + array_shift($parts); + } + + $list = []; + foreach ($parts as $i => $part) { + // Remove empty parts: // and /./ + if ($part === '' || $part === '.') { + continue; + } + + // Resolve /../ by removing path part. + if ($part === '..') { + $test = array_pop($list); + if ($test === null) { + // Oops, user tried to access something outside of our root folder. + throw new RuntimeException("Bad path {$path}"); + } + } else { + $list[] = $part; + } + } + + // Build path back together. + return $root . implode('/', $list); + } +} diff --git a/system/src/Grav/Framework/Filesystem/Interfaces/FilesystemInterface.php b/system/src/Grav/Framework/Filesystem/Interfaces/FilesystemInterface.php new file mode 100644 index 0000000..0e280a8 --- /dev/null +++ b/system/src/Grav/Framework/Filesystem/Interfaces/FilesystemInterface.php @@ -0,0 +1,82 @@ += 1). + * @return string Returns parent path. + * @throws RuntimeException + * @api + */ + public function parent(string $path, int $levels = 1): string; + + /** + * Normalize path by cleaning up `\`, `/./`, `//` and `/../`. + * + * @param string $path A filename or path, does not need to exist as a file. + * @return string Returns normalized path. + * @throws RuntimeException + * @api + */ + public function normalize(string $path): string; + + /** + * Returns filename component of path. + * + * @param string $path A filename or path, does not need to exist as a file. + * @param string|null $suffix If the filename ends in suffix this will also be cut off. + * @return string + * @api + */ + public function basename(string $path, ?string $suffix = null): string; + + /** + * Stream-safe `\dirname()` replacement. + * + * @see http://php.net/manual/en/function.dirname.php + * + * @param string $path A filename or path, does not need to exist as a file. + * @param int $levels The number of parent directories to go up (>= 1). + * @return string Returns path to the directory. + * @throws RuntimeException + * @api + */ + public function dirname(string $path, int $levels = 1): string; + + /** + * Stream-safe `\pathinfo()` replacement. + * + * @see http://php.net/manual/en/function.pathinfo.php + * + * @param string $path A filename or path, does not need to exist as a file. + * @param int|null $options A PATHINFO_* constant. + * @return array|string + * @api + */ + public function pathinfo(string $path, ?int $options = null); +} diff --git a/system/src/Grav/Framework/Flex/Flex.php b/system/src/Grav/Framework/Flex/Flex.php new file mode 100644 index 0000000..ce28521 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Flex.php @@ -0,0 +1,332 @@ + blueprint file, ...] + * @param array $config + */ + public function __construct(array $types, array $config) + { + $this->config = $config; + $this->types = []; + + foreach ($types as $type => $blueprint) { + if (!file_exists($blueprint)) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addMessage(sprintf('Flex: blueprint for flex type %s is missing', $type), 'error'); + + continue; + } + $this->addDirectoryType($type, $blueprint); + } + } + + /** + * @param string $type + * @param string $blueprint + * @param array $config + * @return $this + */ + public function addDirectoryType(string $type, string $blueprint, array $config = []) + { + $config = array_replace_recursive(['enabled' => true], $this->config ?? [], $config); + + $this->types[$type] = new FlexDirectory($type, $blueprint, $config); + + return $this; + } + + /** + * @param FlexDirectory $directory + * @return $this + */ + public function addDirectory(FlexDirectory $directory) + { + $this->types[$directory->getFlexType()] = $directory; + + return $this; + } + + /** + * @param string $type + * @return bool + */ + public function hasDirectory(string $type): bool + { + return isset($this->types[$type]); + } + + /** + * @param array|string[]|null $types + * @param bool $keepMissing + * @return array + */ + public function getDirectories(array $types = null, bool $keepMissing = false): array + { + if ($types === null) { + return $this->types; + } + + // Return the directories in the given order. + $directories = []; + foreach ($types as $type) { + $directories[$type] = $this->types[$type] ?? null; + } + + return $keepMissing ? $directories : array_filter($directories); + } + + /** + * @param string $type + * @return FlexDirectory|null + */ + public function getDirectory(string $type): ?FlexDirectory + { + return $this->types[$type] ?? null; + } + + /** + * @param string $type + * @param array|null $keys + * @param string|null $keyField + * @return FlexCollectionInterface|null + */ + public function getCollection(string $type, array $keys = null, string $keyField = null): ?FlexCollectionInterface + { + $directory = $type ? $this->getDirectory($type) : null; + + return $directory ? $directory->getCollection($keys, $keyField) : null; + } + + /** + * @param array $keys + * @param array $options In addition to the options in getObjects(), following options can be passed: + * collection_class: Class to be used to create the collection. Defaults to ObjectCollection. + * @return FlexCollectionInterface + * @throws RuntimeException + */ + public function getMixedCollection(array $keys, array $options = []): FlexCollectionInterface + { + $collectionClass = $options['collection_class'] ?? ObjectCollection::class; + if (!class_exists($collectionClass)) { + throw new RuntimeException(sprintf('Cannot create collection: Class %s does not exist', $collectionClass)); + } + + $objects = $this->getObjects($keys, $options); + + return new $collectionClass($objects); + } + + /** + * @param array $keys + * @param array $options Following optional options can be passed: + * types: List of allowed types. + * type: Allowed type if types isn't defined, otherwise acts as default_type. + * default_type: Set default type for objects given without type (only used if key_field isn't set). + * keep_missing: Set to true if you want to return missing objects as null. + * key_field: Key field which is used to match the objects. + * @return array + */ + public function getObjects(array $keys, array $options = []): array + { + $type = $options['type'] ?? null; + $defaultType = $options['default_type'] ?? $type ?? null; + $keyField = $options['key_field'] ?? 'flex_key'; + + // Prepare empty result lists for all requested Flex types. + $types = $options['types'] ?? (array)$type ?: null; + if ($types) { + $types = array_fill_keys($types, []); + } + $strict = isset($types); + + $guessed = []; + if ($keyField === 'flex_key') { + // We need to split Flex key lookups into individual directories. + $undefined = []; + $keyFieldFind = 'storage_key'; + + foreach ($keys as $flexKey) { + if (!$flexKey) { + continue; + } + + $flexKey = (string)$flexKey; + // Normalize key and type using fallback to default type if it was set. + [$key, $type, $guess] = $this->resolveKeyAndType($flexKey, $defaultType); + + if ($type === '' && $types) { + // Add keys which are not associated to any Flex type. They will be included to every Flex type. + foreach ($types as $type => &$array) { + $array[] = $key; + $guessed[$key][] = "{$type}.obj:{$key}"; + } + unset($array); + } elseif (!$strict || isset($types[$type])) { + // Collect keys by their Flex type. If allowed types are defined, only include values from those types. + $types[$type][] = $key; + if ($guess) { + $guessed[$key][] = "{$type}.obj:{$key}"; + } + } + } + } else { + // We are using a specific key field, make every key undefined. + $undefined = $keys; + $keyFieldFind = $keyField; + } + + if (!$types) { + return []; + } + + $list = [[]]; + foreach ($types as $type => $typeKeys) { + // Also remember to look up keys from undefined Flex types. + $lookupKeys = $undefined ? array_merge($typeKeys, $undefined) : $typeKeys; + + $collection = $this->getCollection($type, $lookupKeys, $keyFieldFind); + if ($collection && $keyFieldFind !== $keyField) { + $collection = $collection->withKeyField($keyField); + } + + $list[] = $collection ? $collection->toArray() : []; + } + + // Merge objects from individual types back together. + $list = array_merge(...$list); + + // Use the original key ordering. + if (!$guessed) { + $list = array_replace(array_fill_keys($keys, null), $list) ?? []; + } else { + // We have mixed keys, we need to map flex keys back to storage keys. + $results = []; + foreach ($keys as $key) { + $flexKey = $guessed[$key] ?? $key; + if (is_array($flexKey)) { + $result = null; + foreach ($flexKey as $tryKey) { + if ($result = $list[$tryKey] ?? null) { + // Use the first matching object (conflicting objects will be ignored for now). + break; + } + } + } else { + $result = $list[$flexKey] ?? null; + } + + $results[$key] = $result; + } + + $list = $results; + } + + // Remove missing objects if not asked to keep them. + if (empty($options['keep_missing'])) { + $list = array_filter($list); + } + + return $list; + } + + /** + * @param string $key + * @param string|null $type + * @param string|null $keyField + * @return FlexObjectInterface|null + */ + public function getObject(string $key, string $type = null, string $keyField = null): ?FlexObjectInterface + { + if (null === $type && null === $keyField) { + // Special handling for quick Flex key lookups. + $keyField = 'storage_key'; + [$key, $type] = $this->resolveKeyAndType($key, $type); + } else { + $type = $this->resolveType($type); + } + + if ($type === '' || $key === '') { + return null; + } + + $directory = $this->getDirectory($type); + + return $directory ? $directory->getObject($key, $keyField) : null; + } + + /** + * @return int + */ + public function count(): int + { + return count($this->types); + } + + /** + * @param string $flexKey + * @param string|null $type + * @return array + */ + protected function resolveKeyAndType(string $flexKey, string $type = null): array + { + $guess = false; + if (strpos($flexKey, ':') !== false) { + [$type, $key] = explode(':', $flexKey, 2); + + $type = $this->resolveType($type); + } else { + $key = $flexKey; + $type = (string)$type; + $guess = true; + } + + return [$key, $type, $guess]; + } + + /** + * @param string|null $type + * @return string + */ + protected function resolveType(string $type = null): string + { + if (null !== $type && strpos($type, '.') !== false) { + return preg_replace('|\.obj$|', '', $type) ?? $type; + } + + return $type ?? ''; + } +} diff --git a/system/src/Grav/Framework/Flex/FlexCollection.php b/system/src/Grav/Framework/Flex/FlexCollection.php new file mode 100644 index 0000000..54e0665 --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexCollection.php @@ -0,0 +1,712 @@ + + * @implements FlexCollectionInterface + */ +class FlexCollection extends ObjectCollection implements FlexCollectionInterface +{ + /** @var FlexDirectory */ + private $_flexDirectory; + + /** @var string */ + private $_keyField; + + /** + * Get list of cached methods. + * + * @return array Returns a list of methods with their caching information. + */ + public static function getCachedMethods(): array + { + return [ + 'getTypePrefix' => true, + 'getType' => true, + 'getFlexDirectory' => true, + 'hasFlexFeature' => true, + 'getFlexFeatures' => true, + 'getCacheKey' => true, + 'getCacheChecksum' => false, + 'getTimestamp' => true, + 'hasProperty' => true, + 'getProperty' => true, + 'hasNestedProperty' => true, + 'getNestedProperty' => true, + 'orderBy' => true, + + 'render' => false, + 'isAuthorized' => 'session', + 'search' => true, + 'sort' => true, + 'getDistinctValues' => true + ]; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::createFromArray() + */ + public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null) + { + $instance = new static($entries, $directory); + $instance->setKeyField($keyField); + + return $instance; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::__construct() + */ + public function __construct(array $entries = [], FlexDirectory $directory = null) + { + // @phpstan-ignore-next-line + if (get_class($this) === __CLASS__) { + user_error('Using ' . __CLASS__ . ' directly is deprecated since Grav 1.7, use \Grav\Common\Flex\Types\Generic\GenericCollection or your own class instead', E_USER_DEPRECATED); + } + + parent::__construct($entries); + + if ($directory) { + $this->setFlexDirectory($directory)->setKey($directory->getFlexType()); + } + } + + /** + * {@inheritdoc} + * @see FlexCommonInterface::hasFlexFeature() + */ + public function hasFlexFeature(string $name): bool + { + return in_array($name, $this->getFlexFeatures(), true); + } + + /** + * {@inheritdoc} + * @see FlexCommonInterface::hasFlexFeature() + */ + public function getFlexFeatures(): array + { + $implements = class_implements($this); + + $list = []; + foreach ($implements as $interface) { + if ($pos = strrpos($interface, '\\')) { + $interface = substr($interface, $pos+1); + } + + $list[] = Inflector::hyphenize(str_replace('Interface', '', $interface)); + } + + return $list; + + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::search() + */ + public function search(string $search, $properties = null, array $options = null) + { + $matching = $this->call('search', [$search, $properties, $options]); + $matching = array_filter($matching); + + if ($matching) { + arsort($matching, SORT_NUMERIC); + } + + return $this->select(array_keys($matching)); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::sort() + */ + public function sort(array $order) + { + $criteria = Criteria::create()->orderBy($order); + + /** @var FlexCollectionInterface $matching */ + $matching = $this->matching($criteria); + + return $matching; + } + + /** + * @param array $filters + * @return FlexCollectionInterface|Collection + */ + public function filterBy(array $filters) + { + $expr = Criteria::expr(); + $criteria = Criteria::create(); + + foreach ($filters as $key => $value) { + $criteria->andWhere($expr->eq($key, $value)); + } + + return $this->matching($criteria); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexType() + */ + public function getFlexType(): string + { + return $this->_flexDirectory->getFlexType(); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getFlexDirectory(): FlexDirectory + { + return $this->_flexDirectory; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getTimestamp() + */ + public function getTimestamp(): int + { + $timestamps = $this->getTimestamps(); + + return $timestamps ? max($timestamps) : time(); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getCacheKey(): string + { + return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1((string)json_encode($this->call('getKey'))); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getCacheChecksum(): string + { + $list = []; + /** + * @var string $key + * @var FlexObjectInterface $object + */ + foreach ($this as $key => $object) { + $list[$key] = $object->getCacheChecksum(); + } + + return sha1((string)json_encode($list)); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getTimestamps(): array + { + /** @var int[] $timestamps */ + $timestamps = $this->call('getTimestamp'); + + return $timestamps; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getStorageKeys(): array + { + /** @var string[] $keys */ + $keys = $this->call('getStorageKey'); + + return $keys; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getFlexKeys(): array + { + /** @var string[] $keys */ + $keys = $this->call('getFlexKey'); + + return $keys; + } + + /** + * Get all the values in property. + * + * Supports either single scalar values or array of scalar values. + * + * @param string $property Object property to be used to make groups. + * @param string|null $separator Separator, defaults to '.' + * @return array + */ + public function getDistinctValues(string $property, string $separator = null): array + { + $list = []; + + /** @var FlexObjectInterface $element */ + foreach ($this->getIterator() as $element) { + $value = (array)$element->getNestedProperty($property, null, $separator); + foreach ($value as $v) { + if (is_scalar($v)) { + $t = gettype($v) . (string)$v; + $list[$t] = $v; + } + } + } + + return array_values($list); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::withKeyField() + */ + public function withKeyField(string $keyField = null) + { + $keyField = $keyField ?: 'key'; + if ($keyField === $this->getKeyField()) { + return $this; + } + + $entries = []; + foreach ($this as $key => $object) { + // TODO: remove hardcoded logic + if ($keyField === 'storage_key') { + $entries[$object->getStorageKey()] = $object; + } elseif ($keyField === 'flex_key') { + $entries[$object->getFlexKey()] = $object; + } elseif ($keyField === 'key') { + $entries[$object->getKey()] = $object; + } + } + + return $this->createFrom($entries, $keyField); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getIndex() + */ + public function getIndex() + { + return $this->getFlexDirectory()->getIndex($this->getKeys(), $this->getKeyField()); + } + + /** + * @inheritdoc} + * @see FlexCollectionInterface::getCollection() + * @return $this + */ + public function getCollection() + { + return $this; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::render() + */ + public function render(string $layout = null, array $context = []) + { + if (!$layout) { + $config = $this->getTemplateConfig(); + $layout = $config['collection']['defaults']['layout'] ?? 'default'; + } + + $type = $this->getFlexType(); + + $grav = Grav::instance(); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->startTimer('flex-collection-' . ($debugKey = uniqid($type, false)), 'Render Collection ' . $type . ' (' . $layout . ')'); + + $key = null; + foreach ($context as $value) { + if (!is_scalar($value)) { + $key = false; + break; + } + } + + if ($key !== false) { + $key = md5($this->getCacheKey() . '.' . $layout . json_encode($context)); + $cache = $this->getCache('render'); + } else { + $cache = null; + } + + try { + $data = $cache && $key ? $cache->get($key) : null; + + $block = $data ? HtmlBlock::fromArray($data) : null; + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + $block = null; + } catch (\InvalidArgumentException $e) { + $debugger->addException($e); + $block = null; + } + + $checksum = $this->getCacheChecksum(); + if ($block && $checksum !== $block->getChecksum()) { + $block = null; + } + + if (!$block) { + $block = HtmlBlock::create($key ?: null); + $block->setChecksum($checksum); + if (!$key) { + $block->disableCache(); + } + + $event = new Event([ + 'type' => 'flex', + 'directory' => $this->getFlexDirectory(), + 'collection' => $this, + 'layout' => &$layout, + 'context' => &$context + ]); + $this->triggerEvent('onRender', $event); + + $output = $this->getTemplate($layout)->render( + [ + 'grav' => $grav, + 'config' => $grav['config'], + 'block' => $block, + 'directory' => $this->getFlexDirectory(), + 'collection' => $this, + 'layout' => $layout + ] + $context + ); + + if ($debugger->enabled()) { + $output = "\n\n{$output}\n\n"; + } + + $block->setContent($output); + + try { + $cache && $key && $block->isCached() && $cache->set($key, $block->toArray()); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + } + } + + $debugger->stopTimer('flex-collection-' . $debugKey); + + return $block; + } + + /** + * @param FlexDirectory $type + * @return $this + */ + public function setFlexDirectory(FlexDirectory $type) + { + $this->_flexDirectory = $type; + + return $this; + } + + /** + * @param string $key + * @return array + */ + public function getMetaData(string $key): array + { + $object = $this->get($key); + + return $object instanceof FlexObjectInterface ? $object->getMetaData() : []; + } + + /** + * @param string|null $namespace + * @return CacheInterface + */ + public function getCache(string $namespace = null) + { + return $this->_flexDirectory->getCache($namespace); + } + + /** + * @return string + */ + public function getKeyField(): string + { + return $this->_keyField ?? 'storage_key'; + } + + /** + * @param string $action + * @param string|null $scope + * @param UserInterface|null $user + * @return static + * @phpstan-return static + */ + public function isAuthorized(string $action, string $scope = null, UserInterface $user = null) + { + $list = $this->call('isAuthorized', [$action, $scope, $user]); + $list = array_filter($list); + + return $this->select(array_keys($list)); + } + + /** + * @param string $value + * @param string $field + * @return T|null + */ + public function find($value, $field = 'id') + { + if ($value) { + foreach ($this as $element) { + if (mb_strtolower($element->getProperty($field)) === mb_strtolower($value)) { + return $element; + } + } + } + + return null; + } + + /** + * @return array + */ + public function jsonSerialize() + { + $elements = []; + + /** + * @var string $key + * @var array|FlexObject $object + */ + foreach ($this->getElements() as $key => $object) { + $elements[$key] = is_array($object) ? $object : $object->jsonSerialize(); + } + + return $elements; + } + + /** + * @return array + */ + public function __debugInfo() + { + return [ + 'type:private' => $this->getFlexType(), + 'key:private' => $this->getKey(), + 'objects_key:private' => $this->getKeyField(), + 'objects:private' => $this->getElements() + ]; + } + + /** + * Creates a new instance from the specified elements. + * + * This method is provided for derived classes to specify how a new + * instance should be created when constructor semantics have changed. + * + * @param array $elements Elements. + * @param string|null $keyField + * @return static + * @phpstan-return static + * @throws \InvalidArgumentException + */ + protected function createFrom(array $elements, $keyField = null) + { + $collection = new static($elements, $this->_flexDirectory); + $collection->setKeyField($keyField ?: $this->_keyField); + + return $collection; + } + + /** + * @return string + */ + protected function getTypePrefix(): string + { + return 'c.'; + } + + /** + * @return array + */ + protected function getTemplateConfig(): array + { + $config = $this->getFlexDirectory()->getConfig('site.templates', []); + $defaults = array_replace($config['defaults'] ?? [], $config['collection']['defaults'] ?? []); + $config['collection']['defaults'] = $defaults; + + return $config; + } + + /** + * @param string $layout + * @return array + */ + protected function getTemplatePaths(string $layout): array + { + $config = $this->getTemplateConfig(); + $type = $this->getFlexType(); + $defaults = $config['collection']['defaults'] ?? []; + + $ext = $defaults['ext'] ?? '.html.twig'; + $types = array_unique(array_merge([$type], (array)($defaults['type'] ?? null))); + $paths = $config['collection']['paths'] ?? [ + 'flex/{TYPE}/collection/{LAYOUT}{EXT}', + 'flex-objects/layouts/{TYPE}/collection/{LAYOUT}{EXT}' + ]; + $table = ['TYPE' => '%1$s', 'LAYOUT' => '%2$s', 'EXT' => '%3$s']; + + $lookups = []; + foreach ($paths as $path) { + $path = Utils::simpleTemplate($path, $table); + foreach ($types as $type) { + $lookups[] = sprintf($path, $type, $layout, $ext); + } + } + + return array_unique($lookups); + } + + /** + * @param string $layout + * @return Template|TemplateWrapper + * @throws LoaderError + * @throws SyntaxError + */ + protected function getTemplate($layout) + { + $grav = Grav::instance(); + + /** @var Twig $twig */ + $twig = $grav['twig']; + + try { + return $twig->twig()->resolveTemplate($this->getTemplatePaths($layout)); + } catch (LoaderError $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + return $twig->twig()->resolveTemplate(['flex/404.html.twig']); + } + } + + /** + * @param string $type + * @return FlexDirectory + */ + protected function getRelatedDirectory($type): ?FlexDirectory + { + /** @var Flex $flex */ + $flex = Grav::instance()['flex']; + + return $flex->getDirectory($type); + } + + /** + * @param string|null $keyField + * @return void + */ + protected function setKeyField($keyField = null): void + { + $this->_keyField = $keyField ?? 'storage_key'; + } + + // DEPRECATED METHODS + + /** + * @param bool $prefix + * @return string + * @deprecated 1.6 Use `->getFlexType()` instead. + */ + public function getType($prefix = false) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED); + + $type = $prefix ? $this->getTypePrefix() : ''; + + return $type . $this->getFlexType(); + } + + /** + * @param string $name + * @param object|null $event + * @return $this + * @deprecated 1.7, moved to \Grav\Common\Flex\Traits\FlexObjectTrait + */ + public function triggerEvent(string $name, $event = null) + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\FlexObjectTrait', E_USER_DEPRECATED); + + if (null === $event) { + $event = new Event([ + 'type' => 'flex', + 'directory' => $this->getFlexDirectory(), + 'collection' => $this + ]); + } + if (strpos($name, 'onFlexCollection') !== 0 && strpos($name, 'on') === 0) { + $name = 'onFlexCollection' . substr($name, 2); + } + + $grav = Grav::instance(); + if ($event instanceof Event) { + $grav->fireEvent($name, $event); + } else { + $grav->dispatchEvent($event); + } + + + return $this; + } +} diff --git a/system/src/Grav/Framework/Flex/FlexDirectory.php b/system/src/Grav/Framework/Flex/FlexDirectory.php new file mode 100644 index 0000000..29f1490 --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexDirectory.php @@ -0,0 +1,1077 @@ +type = $type; + $this->blueprints = []; + $this->blueprint_file = $blueprint_file; + $this->defaults = $defaults; + $this->enabled = !empty($defaults['enabled']); + $this->objects = []; + } + + /** + * @return bool + */ + public function isListed(): bool + { + $grav = Grav::instance(); + + /** @var Flex $flex */ + $flex = $grav['flex']; + $directory = $flex->getDirectory($this->type); + + return null !== $directory; + } + + /** + * @return bool + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * @return string + */ + public function getFlexType(): string + { + return $this->type; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->getBlueprintInternal()->get('title', ucfirst($this->getFlexType())); + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->getBlueprintInternal()->get('description', ''); + } + + /** + * @param string|null $name + * @param mixed $default + * @return mixed + */ + public function getConfig(string $name = null, $default = null) + { + if (null === $this->config) { + $config = $this->getBlueprintInternal()->get('config', []); + $config = is_array($config) ? array_replace_recursive($config, $this->defaults, $this->getDirectoryConfig($config['admin']['views']['configure']['form'] ?? $config['admin']['configure']['form'] ?? null)) : null; + if (!is_array($config)) { + throw new RuntimeException('Bad configuration'); + } + + $this->config = new Config($config); + } + + return null === $name ? $this->config : $this->config->get($name, $default); + } + + /** + * @param string|null $name + * @param array $options + * @return FlexFormInterface + * @internal + */ + public function getDirectoryForm(string $name = null, array $options = []) + { + $name = $name ?: $this->getConfig('admin.views.configure.form', '') ?: $this->getConfig('admin.configure.form', ''); + + return new FlexDirectoryForm($name ?? '', $this, $options); + } + + /** + * @return Blueprint + * @internal + */ + public function getDirectoryBlueprint() + { + $name = 'configure'; + + $type = $this->getBlueprint(); + $overrides = $type->get("blueprints/{$name}"); + + $path = "blueprints://flex/shared/{$name}.yaml"; + $blueprint = new Blueprint($path); + $blueprint->load(); + if (isset($overrides['fields'])) { + $blueprint->embed('form/fields/tabs/fields', $overrides['fields']); + } + $blueprint->init(); + + return $blueprint; + } + + /** + * @param string $name + * @param array $data + * @return void + * @throws Exception + * @internal + */ + public function saveDirectoryConfig(string $name, array $data) + { + $grav = Grav::instance(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + /** @var string $filename Filename is always string */ + $filename = $locator->findResource($this->getDirectoryConfigUri($name), true, true); + + $file = YamlFile::instance($filename); + if (!empty($data)) { + $file->save($data); + } else { + $file->delete(); + } + } + + /** + * @param string $name + * @return array + * @internal + */ + public function loadDirectoryConfig(string $name): array + { + $grav = Grav::instance(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + $uri = $this->getDirectoryConfigUri($name); + + // If configuration is found in main configuration, use it. + if (str_starts_with($uri, 'config://')) { + $path = str_replace('/', '.', substr($uri, 9, -5)); + + return (array)$grav['config']->get($path); + } + + // Load the configuration file. + $filename = $locator->findResource($uri, true); + if ($filename === false) { + return []; + } + + $file = YamlFile::instance($filename); + + return $file->content(); + } + + /** + * @param string|null $name + * @return string + */ + public function getDirectoryConfigUri(string $name = null): string + { + $name = $name ?: $this->getFlexType(); + $blueprint = $this->getBlueprint(); + + return $blueprint->get('blueprints/views/configure/file') ?? $blueprint->get('blueprints/configure/file') ?? "config://flex/{$name}.yaml"; + } + + /** + * @param string|null $name + * @return array + */ + protected function getDirectoryConfig(string $name = null): array + { + $grav = Grav::instance(); + + /** @var Config $config */ + $config = $grav['config']; + $name = $name ?: $this->getFlexType(); + + return $config->get("flex.{$name}", []); + } + + /** + * Returns a new uninitialized instance of blueprint. + * + * Always use $object->getBlueprint() or $object->getForm()->getBlueprint() instead. + * + * @param string $type + * @param string $context + * @return Blueprint + */ + public function getBlueprint(string $type = '', string $context = '') + { + return clone $this->getBlueprintInternal($type, $context); + } + + /** + * @param string $view + * @return string + */ + public function getBlueprintFile(string $view = ''): string + { + $file = $this->blueprint_file; + if ($view !== '') { + $file = preg_replace('/\.yaml/', "/{$view}.yaml", $file); + } + + return (string)$file; + } + + /** + * Get collection. In the site this will be filtered by the default filters (published etc). + * + * Use $directory->getIndex() if you want unfiltered collection. + * + * @param array|null $keys Array of keys. + * @param string|null $keyField Field to be used as the key. + * @return FlexCollectionInterface + * @phpstan-return FlexCollectionInterface + */ + public function getCollection(array $keys = null, string $keyField = null): FlexCollectionInterface + { + // Get all selected entries. + $index = $this->getIndex($keys, $keyField); + + if (!Utils::isAdminPlugin()) { + // If not in admin, filter the list by using default filters. + $filters = (array)$this->getConfig('site.filter', []); + + foreach ($filters as $filter) { + $index = $index->{$filter}(); + } + } + + return $index; + } + + /** + * Get the full collection of all stored objects. + * + * Use $directory->getCollection() if you want a filtered collection. + * + * @param array|null $keys Array of keys. + * @param string|null $keyField Field to be used as the key. + * @return FlexIndexInterface + */ + public function getIndex(array $keys = null, string $keyField = null): FlexIndexInterface + { + $keyField = $keyField ?? ''; + $index = $this->indexes[$keyField] ?? $this->loadIndex($keyField); + $index = clone $index; + + if (null !== $keys) { + /** @var FlexIndexInterface $index */ + $index = $index->select($keys); + } + + return $index->getIndex(); + } + + /** + * Returns an object if it exists. If no arguments are passed (or both of them are null), method creates a new empty object. + * + * Note: It is not safe to use the object without checking if the user can access it. + * + * @param string|null $key + * @param string|null $keyField Field to be used as the key. + * @return FlexObjectInterface|null + */ + public function getObject($key = null, string $keyField = null): ?FlexObjectInterface + { + if (null === $key) { + return $this->createObject([], ''); + } + + $keyField = $keyField ?? ''; + $index = $this->indexes[$keyField] ?? $this->loadIndex($keyField); + + return $index->get($key); + } + + /** + * @param string|null $namespace + * @return CacheInterface + */ + public function getCache(string $namespace = null) + { + $namespace = $namespace ?: 'index'; + $cache = $this->cache[$namespace] ?? null; + + if (null === $cache) { + try { + $grav = Grav::instance(); + + /** @var Cache $gravCache */ + $gravCache = $grav['cache']; + $config = $this->getConfig('object.cache.' . $namespace); + if (empty($config['enabled'])) { + $cache = new MemoryCache('flex-objects-' . $this->getFlexType()); + } else { + $lifetime = $config['lifetime'] ?? 60; + + $key = $gravCache->getKey(); + if (Utils::isAdminPlugin()) { + $key = substr($key, 0, -1); + } + $cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getFlexType() . $key, $lifetime); + } + } catch (Exception $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + $cache = new MemoryCache('flex-objects-' . $this->getFlexType()); + } + + // Disable cache key validation. + $cache->setValidation(false); + $this->cache[$namespace] = $cache; + } + + return $cache; + } + + /** + * @return $this + */ + public function clearCache() + { + $grav = Grav::instance(); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addMessage(sprintf('Flex: Clearing all %s cache', $this->type), 'debug'); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + $locator->clearCache(); + + $this->getCache('index')->clear(); + $this->getCache('object')->clear(); + $this->getCache('render')->clear(); + + $this->indexes = []; + $this->objects = []; + + return $this; + } + + /** + * @param string|null $key + * @return string|null + */ + public function getStorageFolder(string $key = null): ?string + { + return $this->getStorage()->getStoragePath($key); + } + + /** + * @param string|null $key + * @return string|null + */ + public function getMediaFolder(string $key = null): ?string + { + return $this->getStorage()->getMediaPath($key); + } + + /** + * @return FlexStorageInterface + */ + public function getStorage(): FlexStorageInterface + { + if (null === $this->storage) { + $this->storage = $this->createStorage(); + } + + return $this->storage; + } + + /** + * @param array $data + * @param string $key + * @param bool $validate + * @return FlexObjectInterface + */ + public function createObject(array $data, string $key = '', bool $validate = false): FlexObjectInterface + { + /** @var string|FlexObjectInterface $className */ + $className = $this->objectClassName ?: $this->getObjectClass(); + + return new $className($data, $key, $this, $validate); + } + + /** + * @param array $entries + * @param string|null $keyField + * @return FlexCollectionInterface + */ + public function createCollection(array $entries, string $keyField = null): FlexCollectionInterface + { + /** @var string|FlexCollectionInterface $className */ + $className = $this->collectionClassName ?: $this->getCollectionClass(); + + return $className::createFromArray($entries, $this, $keyField); + } + + /** + * @param array $entries + * @param string|null $keyField + * @return FlexIndexInterface + */ + public function createIndex(array $entries, string $keyField = null): FlexIndexInterface + { + /** @var string|FlexIndexInterface $className */ + $className = $this->indexClassName ?: $this->getIndexClass(); + + return $className::createFromArray($entries, $this, $keyField); + } + + /** + * @return string + */ + public function getObjectClass(): string + { + if (!$this->objectClassName) { + $this->objectClassName = $this->getConfig('data.object', GenericObject::class); + } + + return $this->objectClassName; + } + + /** + * @return string + */ + public function getCollectionClass(): string + { + if (!$this->collectionClassName) { + $this->collectionClassName = $this->getConfig('data.collection', GenericCollection::class); + } + + return $this->collectionClassName; + } + + + /** + * @return string + */ + public function getIndexClass(): string + { + if (!$this->indexClassName) { + $this->indexClassName = $this->getConfig('data.index', GenericIndex::class); + } + + return $this->indexClassName; + } + + /** + * @param array $entries + * @param string|null $keyField + * @return FlexCollectionInterface + */ + public function loadCollection(array $entries, string $keyField = null): FlexCollectionInterface + { + return $this->createCollection($this->loadObjects($entries), $keyField); + } + + /** + * @param array $entries + * @return FlexObjectInterface[] + * @internal + */ + public function loadObjects(array $entries): array + { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + + $keys = []; + $rows = []; + $fetch = []; + + // Build lookup arrays with storage keys for the objects. + foreach ($entries as $key => $value) { + $k = $value['storage_key'] ?? ''; + if ($k === '') { + continue; + } + $v = $this->objects[$k] ?? null; + $keys[$k] = $key; + $rows[$k] = $v; + if (!$v) { + $fetch[] = $k; + } + } + + // Attempt to fetch missing rows from the cache. + if ($fetch) { + $rows = (array)array_replace($rows, $this->loadCachedObjects($fetch)); + } + + // Read missing rows from the storage. + $updated = []; + $storage = $this->getStorage(); + $rows = $storage->readRows($rows, $updated); + + // Create objects from the rows. + $isListed = $this->isListed(); + $list = []; + foreach ($rows as $storageKey => $row) { + $usedKey = $keys[$storageKey]; + + if ($row instanceof FlexObjectInterface) { + $object = $row; + } else { + if ($row === null) { + $debugger->addMessage(sprintf('Flex: Object %s was not found from %s storage', $storageKey, $this->type), 'debug'); + continue; + } + + if (isset($row['__ERROR'])) { + $message = sprintf('Flex: Object %s is broken in %s storage: %s', $storageKey, $this->type, $row['__ERROR']); + $debugger->addException(new RuntimeException($message)); + $debugger->addMessage($message, 'error'); + continue; + } + + if (!isset($row['__META'])) { + $row['__META'] = [ + 'storage_key' => $storageKey, + 'storage_timestamp' => $entries[$usedKey]['storage_timestamp'] ?? 0, + ]; + } + + $key = $row['__META']['key'] ?? $entries[$usedKey]['key'] ?? $usedKey; + $object = $this->createObject($row, $key, false); + $this->objects[$storageKey] = $object; + if ($isListed) { + // If unserialize works for the object, serialize the object to speed up the loading. + $updated[$storageKey] = $object; + } + } + + $list[$usedKey] = $object; + } + + // Store updated rows to the cache. + if ($updated) { + $cache = $this->getCache('object'); + if (!$cache instanceof MemoryCache) { + ///** @var Debugger $debugger */ + //$debugger = Grav::instance()['debugger']; + //$debugger->addMessage(sprintf('Flex: Caching %d %s', \count($entries), $this->type), 'debug'); + } + try { + $cache->setMultiple($updated); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + // TODO: log about the issue. + } + } + + if ($fetch) { + $debugger->stopTimer('flex-objects'); + } + + return $list; + } + + protected function loadCachedObjects(array $fetch): array + { + if (!$fetch) { + return []; + } + + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + + $cache = $this->getCache('object'); + + // Attempt to fetch missing rows from the cache. + $fetched = []; + try { + $loading = count($fetch); + + $debugger->startTimer('flex-objects', sprintf('Flex: Loading %d %s', $loading, $this->type)); + + $fetched = (array)$cache->getMultiple($fetch); + if ($fetched) { + $index = $this->loadIndex('storage_key'); + + // Make sure cached objects are up to date: compare against index checksum/timestamp. + /** + * @var string $key + * @var mixed $value + */ + foreach ($fetched as $key => $value) { + if ($value instanceof FlexObjectInterface) { + $objectMeta = $value->getMetaData(); + } else { + $objectMeta = $value['__META'] ?? []; + } + $indexMeta = $index->getMetaData($key); + + $indexChecksum = $indexMeta['checksum'] ?? $indexMeta['storage_timestamp'] ?? null; + $objectChecksum = $objectMeta['checksum'] ?? $objectMeta['storage_timestamp'] ?? null; + if ($indexChecksum !== $objectChecksum) { + unset($fetched[$key]); + } + } + } + + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + } + + return $fetched; + } + + /** + * @return void + */ + public function reloadIndex(): void + { + $this->getCache('index')->clear(); + + $this->indexes = []; + $this->objects = []; + } + + /** + * @param string $scope + * @param string $action + * @return string + */ + public function getAuthorizeRule(string $scope, string $action): string + { + if (!$this->_authorize) { + $config = $this->getConfig('admin.permissions'); + if ($config) { + $this->_authorize = array_key_first($config) . '.%2$s'; + } else { + $this->_authorize = '%1$s.flex-object.%2$s'; + } + } + + return sprintf($this->_authorize, $scope, $action); + } + + /** + * @param string $type_view + * @param string $context + * @return Blueprint + */ + protected function getBlueprintInternal(string $type_view = '', string $context = '') + { + if (!isset($this->blueprints[$type_view])) { + if (!file_exists($this->blueprint_file)) { + throw new RuntimeException(sprintf('Flex: Blueprint file for %s is missing', $this->type)); + } + + $parts = explode('.', rtrim($type_view, '.'), 2); + $type = array_shift($parts); + $view = array_shift($parts) ?: ''; + + $blueprint = new Blueprint($this->getBlueprintFile($view)); + $blueprint->addDynamicHandler('data', function (array &$field, $property, array &$call) { + $this->dynamicDataField($field, $property, $call); + }); + $blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) { + $this->dynamicFlexField($field, $property, $call); + }); + + if ($context) { + $blueprint->setContext($context); + } + + $blueprint->load($type ?: null); + if ($blueprint->get('type') === 'flex-objects' && isset(Grav::instance()['admin'])) { + $blueprintBase = (new Blueprint('plugin://flex-objects/blueprints/flex-objects.yaml'))->load(); + $blueprint->extend($blueprintBase, true); + } + + $this->blueprints[$type_view] = $blueprint; + } + + return $this->blueprints[$type_view]; + } + + /** + * @param array $field + * @param string $property + * @param array $call + * @return void + */ + protected function dynamicDataField(array &$field, $property, array $call) + { + $params = $call['params']; + if (is_array($params)) { + $function = array_shift($params); + } else { + $function = $params; + $params = []; + } + + $object = $call['object']; + if ($function === '\Grav\Common\Page\Pages::pageTypes') { + $params = [$object instanceof PageInterface && $object->isModule() ? 'modular' : 'standard']; + } + + $data = null; + if (is_callable($function)) { + $data = call_user_func_array($function, $params); + } + + // If function returns a value, + if (null !== $data) { + if (is_array($data) && isset($field[$property]) && is_array($field[$property])) { + // Combine field and @data-field together. + $field[$property] += $data; + } else { + // Or create/replace field with @data-field. + $field[$property] = $data; + } + } + } + + /** + * @param array $field + * @param string $property + * @param array $call + * @return void + */ + protected function dynamicFlexField(array &$field, $property, array $call): void + { + $params = (array)$call['params']; + $object = $call['object'] ?? null; + $method = array_shift($params); + $not = false; + if (str_starts_with($method, '!')) { + $method = substr($method, 1); + $not = true; + } elseif (str_starts_with($method, 'not ')) { + $method = substr($method, 4); + $not = true; + } + $method = trim($method); + + if ($object && method_exists($object, $method)) { + $value = $object->{$method}(...$params); + if (is_array($value) && isset($field[$property]) && is_array($field[$property])) { + $value = $this->mergeArrays($field[$property], $value); + } + $field[$property] = $not ? !$value : $value; + } + } + + /** + * @param array $array1 + * @param array $array2 + * @return array + */ + protected function mergeArrays(array $array1, array $array2): array + { + foreach ($array2 as $key => $value) { + if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) { + $array1[$key] = $this->mergeArrays($array1[$key], $value); + } else { + $array1[$key] = $value; + } + } + + return $array1; + } + + /** + * @return FlexStorageInterface + */ + protected function createStorage(): FlexStorageInterface + { + $this->collection = $this->createCollection([]); + + $storage = $this->getConfig('data.storage'); + + if (!is_array($storage)) { + $storage = ['options' => ['folder' => $storage]]; + } + + $className = $storage['class'] ?? SimpleStorage::class; + $options = $storage['options'] ?? []; + + return new $className($options); + } + + /** + * @param string $keyField + * @return FlexIndexInterface + */ + protected function loadIndex(string $keyField): FlexIndexInterface + { + static $i = 0; + + $index = $this->indexes[$keyField] ?? null; + if (null !== $index) { + return $index; + } + + $index = $this->indexes['storage_key'] ?? null; + if (null === $index) { + $i++; + $j = $i; + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->startTimer('flex-keys-' . $this->type . $j, "Flex: Loading {$this->type} index"); + + $storage = $this->getStorage(); + $cache = $this->getCache('index'); + + try { + $keys = $cache->get('__keys'); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + $keys = null; + } + + if (!is_array($keys)) { + /** @var string|FlexIndexInterface $className */ + $className = $this->getIndexClass(); + $keys = $className::loadEntriesFromStorage($storage); + if (!$cache instanceof MemoryCache) { + $debugger->addMessage( + sprintf('Flex: Caching %s index of %d objects', $this->type, count($keys)), + 'debug' + ); + } + try { + $cache->set('__keys', $keys); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + // TODO: log about the issue. + } + } + + $ordering = $this->getConfig('data.ordering', []); + + // We need to do this in two steps as orderBy() calls loadIndex() again and we do not want infinite loop. + $this->indexes['storage_key'] = $index = $this->createIndex($keys, 'storage_key'); + if ($ordering) { + /** @var FlexCollectionInterface $collection */ + $collection = $this->indexes['storage_key']->orderBy($ordering); + $this->indexes['storage_key'] = $index = $collection->getIndex(); + } + + $debugger->stopTimer('flex-keys-' . $this->type . $j); + } + + if ($keyField !== 'storage_key') { + $this->indexes[$keyField] = $index = $index->withKeyField($keyField ?: null); + } + + return $index; + } + + /** + * @param string $action + * @return string + */ + protected function getAuthorizeAction(string $action): string + { + // Handle special action save, which can mean either update or create. + if ($action === 'save') { + $action = 'create'; + } + + return $action; + } + /** + * @return UserInterface|null + */ + protected function getActiveUser(): ?UserInterface + { + /** @var UserInterface|null $user */ + $user = Grav::instance()['user'] ?? null; + + return $user; + } + + /** + * @return string + */ + protected function getAuthorizeScope(): string + { + return isset(Grav::instance()['admin']) ? 'admin' : 'site'; + } + + // DEPRECATED METHODS + + /** + * @return string + * @deprecated 1.6 Use ->getFlexType() method instead. + */ + public function getType(): string + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED); + + return $this->type; + } + + /** + * @param array $data + * @param string|null $key + * @return FlexObjectInterface + * @deprecated 1.7 Use $object->update()->save() instead. + */ + public function update(array $data, string $key = null): FlexObjectInterface + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() should not be used anymore: use $object->update()->save() instead.', E_USER_DEPRECATED); + + $object = null !== $key ? $this->getIndex()->get($key): null; + + $storage = $this->getStorage(); + + if (null === $object) { + $object = $this->createObject($data, $key ?? '', true); + $key = $object->getStorageKey(); + + if ($key) { + $storage->replaceRows([$key => $object->prepareStorage()]); + } else { + $storage->createRows([$object->prepareStorage()]); + } + } else { + $oldKey = $object->getStorageKey(); + $object->update($data); + $newKey = $object->getStorageKey(); + + if ($oldKey !== $newKey) { + $object->triggerEvent('move'); + $storage->renameRow($oldKey, $newKey); + // TODO: media support. + } + + $object->save(); + } + + try { + $this->clearCache(); + } catch (InvalidArgumentException $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + // Caching failed, but we can ignore that for now. + } + + return $object; + } + + /** + * @param string $key + * @return FlexObjectInterface|null + * @deprecated 1.7 Use $object->delete() instead. + */ + public function remove(string $key): ?FlexObjectInterface + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() should not be used anymore: use $object->delete() instead.', E_USER_DEPRECATED); + + $object = $this->getIndex()->get($key); + if (!$object) { + return null; + } + + $object->delete(); + + return $object; + } +} diff --git a/system/src/Grav/Framework/Flex/FlexDirectoryForm.php b/system/src/Grav/Framework/Flex/FlexDirectoryForm.php new file mode 100644 index 0000000..8d608fd --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexDirectoryForm.php @@ -0,0 +1,503 @@ +getDirectoryForm($name, $options); + } + + /** + * FlexForm constructor. + * @param string $name + * @param FlexDirectory $directory + * @param array|null $options + */ + public function __construct(string $name, FlexDirectory $directory, array $options = null) + { + $this->name = $name; + $this->setDirectory($directory); + $this->setName($directory->getFlexType(), $name); + $this->setId($this->getName()); + + $uniqueId = $options['unique_id'] ?? null; + if (!$uniqueId) { + $uniqueId = md5($directory->getFlexType() . '-directory-' . $this->name); + } + $this->setUniqueId($uniqueId); + + $this->setFlashLookupFolder($directory->getDirectoryBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]'); + $this->form = $options['form'] ?? null; + + if (Utils::isPositive($this->form['disabled'] ?? false)) { + $this->disable(); + } + + $this->initialize(); + } + + /** + * @return $this + */ + public function initialize() + { + $this->messages = []; + $this->submitted = false; + $this->data = new Data($this->directory->loadDirectoryConfig($this->name), $this->getBlueprint()); + $this->files = []; + $this->unsetFlash(); + + /** @var FlexFormFlash $flash */ + $flash = $this->getFlash(); + if ($flash->exists()) { + $data = $flash->getData(); + $includeOriginal = (bool)($this->getBlueprint()->form()['images']['original'] ?? null); + + $directory = $flash->getDirectory(); + if (null === $directory) { + throw new RuntimeException('Flash has no directory'); + } + $this->directory = $directory; + $this->data = $data ? new Data($data, $this->getBlueprint()) : null; + $this->files = $flash->getFilesByFields($includeOriginal); + } + + return $this; + } + + /** + * @param string $uniqueId + * @return void + */ + public function setUniqueId(string $uniqueId): void + { + if ($uniqueId !== '') { + $this->uniqueid = $uniqueId; + } + } + + /** + * @param string $name + * @param mixed $default + * @param string|null $separator + * @return mixed + */ + public function get($name, $default = null, $separator = null) + { + switch (strtolower($name)) { + case 'id': + case 'uniqueid': + case 'name': + case 'noncename': + case 'nonceaction': + case 'action': + case 'data': + case 'files': + case 'errors'; + case 'fields': + case 'blueprint': + case 'page': + $method = 'get' . $name; + return $this->{$method}(); + } + + return $this->traitGet($name, $default, $separator); + } + + /** + * @param string $name + * @param mixed $value + * @param string|null $separator + * @return $this + */ + public function set($name, $value, $separator = null) + { + switch (strtolower($name)) { + case 'id': + case 'uniqueid': + $method = 'set' . $name; + return $this->{$method}(); + } + + return $this->traitSet($name, $value, $separator); + } + + /** + * @return string + */ + public function getName(): string + { + return $this->flexName; + } + + protected function setName(string $type, string $name): void + { + // Make sure that both type and name do not have dash (convert dashes to underscores). + $type = str_replace('-', '_', $type); + $name = str_replace('-', '_', $name); + $this->flexName = $name ? "flex_conf-{$type}-{$name}" : "flex_conf-{$type}"; + } + + /** + * @return Data|object + */ + public function getData() + { + if (null === $this->data) { + $this->data = new Data([], $this->getBlueprint()); + } + + return $this->data; + } + + /** + * Get a value from the form. + * + * Note: Used in form fields. + * + * @param string $name + * @return mixed + */ + public function getValue(string $name) + { + // Attempt to get value from the form data. + $value = $this->data ? $this->data[$name] : null; + + // Return the form data or fall back to the object property. + return $value ?? null; + } + + /** + * @param string $name + * @return array|mixed|null + */ + public function getDefaultValue(string $name) + { + return $this->getBlueprint()->getDefaultValue($name); + } + + /** + * @return array + */ + public function getDefaultValues(): array + { + return $this->getBlueprint()->getDefaults(); + } + /** + * @return string + */ + public function getFlexType(): string + { + return $this->directory->getFlexType(); + } + + /** + * Get form flash object. + * + * @return FormFlashInterface|FlexFormFlash + */ + public function getFlash() + { + if (null === $this->flash) { + $grav = Grav::instance(); + $config = [ + 'session_id' => $this->getSessionId(), + 'unique_id' => $this->getUniqueId(), + 'form_name' => $this->getName(), + 'folder' => $this->getFlashFolder(), + 'directory' => $this->getDirectory() + ]; + + $this->flash = new FlexFormFlash($config); + $this->flash + ->setUrl($grav['uri']->url) + ->setUser($grav['user'] ?? null); + } + + return $this->flash; + } + + /** + * @return FlexDirectory + */ + public function getDirectory(): FlexDirectory + { + return $this->directory; + } + + /** + * @return Blueprint + */ + public function getBlueprint(): Blueprint + { + if (null === $this->blueprint) { + try { + $blueprint = $this->getDirectory()->getDirectoryBlueprint(); + if ($this->form) { + // We have field overrides available. + $blueprint->extend(['form' => $this->form], true); + $blueprint->init(); + } + } catch (RuntimeException $e) { + if (!isset($this->form['fields'])) { + throw $e; + } + + // Blueprint is not defined, but we have custom form fields available. + $blueprint = new Blueprint(null, ['form' => $this->form]); + $blueprint->load(); + $blueprint->setScope('directory'); + $blueprint->init(); + } + + $this->blueprint = $blueprint; + } + + return $this->blueprint; + } + + /** + * @return Route|null + */ + public function getFileUploadAjaxRoute(): ?Route + { + return null; + } + + /** + * @param string|null $field + * @param string|null $filename + * @return Route|null + */ + public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route + { + return null; + } + + /** + * @param array $params + * @param string|null $extension + * @return string + */ + public function getMediaTaskRoute(array $params = [], string $extension = null): string + { + return ''; + } + + /** + * @param string $name + * @return mixed|null + */ + public function __get($name) + { + $method = "get{$name}"; + if (method_exists($this, $method)) { + return $this->{$method}(); + } + + $form = $this->getBlueprint()->form(); + + return $form[$name] ?? null; + } + + /** + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) + { + $method = "set{$name}"; + if (method_exists($this, $method)) { + $this->{$method}($value); + } + } + + /** + * @param string $name + * @return bool + */ + public function __isset($name) + { + $method = "get{$name}"; + if (method_exists($this, $method)) { + return true; + } + + $form = $this->getBlueprint()->form(); + + return isset($form[$name]); + } + + /** + * @param string $name + * @return void + */ + public function __unset($name) + { + } + + /** + * @return array|bool + */ + protected function getUnserializeAllowedClasses() + { + return [FlexObject::class]; + } + + /** + * Note: this method clones the object. + * + * @param FlexDirectory $directory + * @return $this + */ + protected function setDirectory(FlexDirectory $directory): self + { + $this->directory = $directory; + + return $this; + } + + /** + * @param string $layout + * @return Template|TemplateWrapper + * @throws LoaderError + * @throws SyntaxError + */ + protected function getTemplate($layout) + { + $grav = Grav::instance(); + + /** @var Twig $twig */ + $twig = $grav['twig']; + + return $twig->twig()->resolveTemplate( + [ + "flex-objects/layouts/{$this->getFlexType()}/form/{$layout}.html.twig", + "flex-objects/layouts/_default/form/{$layout}.html.twig", + "forms/{$layout}/form.html.twig", + 'forms/default/form.html.twig' + ] + ); + } + + /** + * @param array $data + * @param array $files + * @return void + * @throws Exception + */ + protected function doSubmit(array $data, array $files) + { + $this->directory->saveDirectoryConfig($this->name, $data); + + $this->reset(); + } + + /** + * @return array + */ + protected function doSerialize(): array + { + return $this->doTraitSerialize() + [ + 'form' => $this->form, + 'directory' => $this->directory, + 'flexName' => $this->flexName + ]; + } + + /** + * @param array $data + * @return void + */ + protected function doUnserialize(array $data): void + { + $this->doTraitUnserialize($data); + + $this->form = $data['form']; + $this->directory = $data['directory']; + $this->flexName = $data['flexName']; + } + + /** + * Filter validated data. + * + * @param ArrayAccess|Data|null $data + */ + protected function filterData($data = null): void + { + if ($data instanceof Data) { + $data->filter(false, true); + } + } +} diff --git a/system/src/Grav/Framework/Flex/FlexForm.php b/system/src/Grav/Framework/Flex/FlexForm.php new file mode 100644 index 0000000..d25cefc --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexForm.php @@ -0,0 +1,604 @@ +getObject($key) ?? $directory->createObject([], $key); + } else { + throw new RuntimeException(__METHOD__ . "(): You need to pass option 'directory' or 'object'", 400); + } + + $name = $options['name'] ?? ''; + + // There is no reason to pass object and directory. + unset($options['object'], $options['directory']); + + return $object->getForm($name, $options); + } + + /** + * FlexForm constructor. + * @param string $name + * @param FlexObjectInterface $object + * @param array|null $options + */ + public function __construct(string $name, FlexObjectInterface $object, array $options = null) + { + $this->name = $name; + $this->setObject($object); + + if (isset($options['form']['name'])) { + // Use custom form name. + $this->flexName = $options['form']['name']; + } else { + // Use standard form name. + $this->setName($object->getFlexType(), $name); + } + $this->setId($this->getName()); + + $uniqueId = $options['unique_id'] ?? null; + if (!$uniqueId) { + if ($object->exists()) { + $uniqueId = $object->getStorageKey(); + } elseif ($object->hasKey()) { + $uniqueId = "{$object->getKey()}:new"; + } else { + $uniqueId = "{$object->getFlexType()}:new"; + } + $uniqueId = md5($uniqueId); + } + $this->setUniqueId($uniqueId); + + $directory = $object->getFlexDirectory(); + $this->setFlashLookupFolder($options['flash_folder'] ?? $directory->getBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]'); + $this->form = $options['form'] ?? null; + + if (Utils::isPositive($this->items['disabled'] ?? $this->form['disabled'] ?? false)) { + $this->disable(); + } + + if (!empty($options['reset'])) { + $this->getFlash()->delete(); + } + + $this->initialize(); + } + + /** + * @return $this + */ + public function initialize() + { + $this->messages = []; + $this->submitted = false; + $this->data = null; + $this->files = []; + $this->unsetFlash(); + + /** @var FlexFormFlash $flash */ + $flash = $this->getFlash(); + if ($flash->exists()) { + $data = $flash->getData(); + if (null !== $data) { + $data = new Data($data, $this->getBlueprint()); + $data->setKeepEmptyValues(true); + $data->setMissingValuesAsNull(true); + } + + $object = $flash->getObject(); + if (null === $object) { + throw new RuntimeException('Flash has no object'); + } + + $this->object = $object; + $this->data = $data; + + $includeOriginal = (bool)($this->getBlueprint()->form()['images']['original'] ?? null); + $this->files = $flash->getFilesByFields($includeOriginal); + } + + return $this; + } + + /** + * @param string $uniqueId + * @return void + */ + public function setUniqueId(string $uniqueId): void + { + if ($uniqueId !== '') { + $this->uniqueid = $uniqueId; + } + } + + /** + * @param string $name + * @param mixed $default + * @param string|null $separator + * @return mixed + */ + public function get($name, $default = null, $separator = null) + { + switch (strtolower($name)) { + case 'id': + case 'uniqueid': + case 'name': + case 'noncename': + case 'nonceaction': + case 'action': + case 'data': + case 'files': + case 'errors'; + case 'fields': + case 'blueprint': + case 'page': + $method = 'get' . $name; + return $this->{$method}(); + } + + return $this->traitGet($name, $default, $separator); + } + + /** + * @param string $name + * @param mixed $value + * @param string|null $separator + * @return FlexForm + */ + public function set($name, $value, $separator = null) + { + switch (strtolower($name)) { + case 'id': + case 'uniqueid': + $method = 'set' . $name; + return $this->{$method}(); + } + + return $this->traitSet($name, $value, $separator); + } + + /** + * @return string + */ + public function getName(): string + { + return $this->flexName; + } + + /** + * @param callable|null $submitMethod + */ + public function setSubmitMethod(?callable $submitMethod): void + { + $this->submitMethod = $submitMethod; + } + + /** + * @param string $type + * @param string $name + */ + protected function setName(string $type, string $name): void + { + // Make sure that both type and name do not have dash (convert dashes to underscores). + $type = str_replace('-', '_', $type); + $name = str_replace('-', '_', $name); + $this->flexName = $name ? "flex-{$type}-{$name}" : "flex-{$type}"; + } + + /** + * @return Data|FlexObjectInterface|object + */ + public function getData() + { + return $this->data ?? $this->getObject(); + } + + /** + * Get a value from the form. + * + * Note: Used in form fields. + * + * @param string $name + * @return mixed + */ + public function getValue(string $name) + { + // Attempt to get value from the form data. + $value = $this->data ? $this->data[$name] : null; + + // Return the form data or fall back to the object property. + return $value ?? $this->getObject()->getFormValue($name); + } + + /** + * @param string $name + * @return array|mixed|null + */ + public function getDefaultValue(string $name) + { + return $this->object->getDefaultValue($name); + } + + /** + * @return array + */ + public function getDefaultValues(): array + { + return $this->object->getDefaultValues(); + } + /** + * @return string + */ + public function getFlexType(): string + { + return $this->object->getFlexType(); + } + + /** + * Get form flash object. + * + * @return FormFlashInterface|FlexFormFlash + */ + public function getFlash() + { + if (null === $this->flash) { + $grav = Grav::instance(); + $config = [ + 'session_id' => $this->getSessionId(), + 'unique_id' => $this->getUniqueId(), + 'form_name' => $this->getName(), + 'folder' => $this->getFlashFolder(), + 'object' => $this->getObject() + ]; + + $this->flash = new FlexFormFlash($config); + $this->flash + ->setUrl($grav['uri']->url) + ->setUser($grav['user'] ?? null); + } + + return $this->flash; + } + + /** + * @return FlexObjectInterface + */ + public function getObject(): FlexObjectInterface + { + return $this->object; + } + + /** + * @return FlexObjectInterface + */ + public function updateObject(): FlexObjectInterface + { + $data = $this->data instanceof Data ? $this->data->toArray() : []; + $files = $this->files; + + return $this->getObject()->update($data, $files); + } + + /** + * @return Blueprint + */ + public function getBlueprint(): Blueprint + { + if (null === $this->blueprint) { + try { + $blueprint = $this->getObject()->getBlueprint($this->name); + if ($this->form) { + // We have field overrides available. + $blueprint->extend(['form' => $this->form], true); + $blueprint->init(); + } + } catch (RuntimeException $e) { + if (!isset($this->form['fields'])) { + throw $e; + } + + // Blueprint is not defined, but we have custom form fields available. + $blueprint = new Blueprint(null, ['form' => $this->form]); + $blueprint->load(); + $blueprint->setScope('object'); + $blueprint->init(); + } + + $this->blueprint = $blueprint; + } + + return $this->blueprint; + } + + /** + * @return Route|null + */ + public function getFileUploadAjaxRoute(): ?Route + { + $object = $this->getObject(); + if (!method_exists($object, 'route')) { + /** @var Route $route */ + $route = Grav::instance()['route']; + + return $route->withExtension('json')->withGravParam('task', 'media.upload'); + } + + return $object->route('/edit.json/task:media.upload'); + } + + /** + * @param string|null $field + * @param string|null $filename + * @return Route|null + */ + public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route + { + $object = $this->getObject(); + if (!method_exists($object, 'route')) { + /** @var Route $route */ + $route = Grav::instance()['route']; + + return $route->withExtension('json')->withGravParam('task', 'media.delete'); + } + + return $object->route('/edit.json/task:media.delete'); + } + + /** + * @param array $params + * @param string|null $extension + * @return string + */ + public function getMediaTaskRoute(array $params = [], string $extension = null): string + { + $grav = Grav::instance(); + /** @var Flex $flex */ + $flex = $grav['flex_objects']; + + if (method_exists($flex, 'adminRoute')) { + return $flex->adminRoute($this->getObject(), $params, $extension ?? 'json'); + } + + return ''; + } + + /** + * @param string $name + * @return mixed|null + */ + public function __get($name) + { + $method = "get{$name}"; + if (method_exists($this, $method)) { + return $this->{$method}(); + } + + $form = $this->getBlueprint()->form(); + + return $form[$name] ?? null; + } + + /** + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) + { + $method = "set{$name}"; + if (method_exists($this, $method)) { + $this->{$method}($value); + } + } + + /** + * @param string $name + * @return bool + */ + public function __isset($name) + { + $method = "get{$name}"; + if (method_exists($this, $method)) { + return true; + } + + $form = $this->getBlueprint()->form(); + + return isset($form[$name]); + } + + /** + * @param string $name + * @return void + */ + public function __unset($name) + { + } + + /** + * @return array|bool + */ + protected function getUnserializeAllowedClasses() + { + return [FlexObject::class]; + } + + /** + * Note: this method clones the object. + * + * @param FlexObjectInterface $object + * @return $this + */ + protected function setObject(FlexObjectInterface $object): self + { + $this->object = clone $object; + + return $this; + } + + /** + * @param string $layout + * @return Template|TemplateWrapper + * @throws LoaderError + * @throws SyntaxError + */ + protected function getTemplate($layout) + { + $grav = Grav::instance(); + + /** @var Twig $twig */ + $twig = $grav['twig']; + + return $twig->twig()->resolveTemplate( + [ + "flex-objects/layouts/{$this->getFlexType()}/form/{$layout}.html.twig", + "flex-objects/layouts/_default/form/{$layout}.html.twig", + "forms/{$layout}/form.html.twig", + 'forms/default/form.html.twig' + ] + ); + } + + /** + * @param array $data + * @param array $files + * @return void + * @throws Exception + */ + protected function doSubmit(array $data, array $files) + { + /** @var FlexObject $object */ + $object = clone $this->getObject(); + + $method = $this->submitMethod; + if ($method) { + $method($data, $files, $object); + } else { + $object->update($data, $files); + $object->save(); + } + + $this->setObject($object); + $this->reset(); + } + + /** + * @return array + */ + protected function doSerialize(): array + { + return $this->doTraitSerialize() + [ + 'items' => $this->items, + 'form' => $this->form, + 'object' => $this->object, + 'flexName' => $this->flexName, + 'submitMethod' => $this->submitMethod, + ]; + } + + /** + * @param array $data + * @return void + */ + protected function doUnserialize(array $data): void + { + $this->doTraitUnserialize($data); + + $this->items = $data['items'] ?? null; + $this->form = $data['form'] ?? null; + $this->object = $data['object'] ?? null; + $this->flexName = $data['flexName'] ?? null; + $this->submitMethod = $data['submitMethod'] ?? null; + } + + /** + * Filter validated data. + * + * @param ArrayAccess|Data|null $data + * @return void + */ + protected function filterData($data = null): void + { + if ($data instanceof Data) { + $data->filter(true, true); + } + } +} diff --git a/system/src/Grav/Framework/Flex/FlexFormFlash.php b/system/src/Grav/Framework/Flex/FlexFormFlash.php new file mode 100644 index 0000000..feb7a9e --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexFormFlash.php @@ -0,0 +1,130 @@ +object = $object; + $this->directory = $object->getFlexDirectory(); + } + + /** + * @return FlexObjectInterface|null + */ + public function getObject(): ?FlexObjectInterface + { + return $this->object; + } + + /** + * @param FlexDirectory $directory + */ + public function setDirectory(FlexDirectory $directory): void + { + $this->directory = $directory; + } + + /** + * @return FlexDirectory|null + */ + public function getDirectory(): ?FlexDirectory + { + return $this->directory; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + $serialized = parent::jsonSerialize(); + + $object = $this->getObject(); + if ($object instanceof FlexObjectInterface) { + $serialized['object'] = [ + 'type' => $object->getFlexType(), + 'key' => $object->getKey() ?: null, + 'storage_key' => $object->getStorageKey(), + 'timestamp' => $object->getTimestamp(), + 'serialized' => $object->prepareStorage() + ]; + } else { + $directory = $this->getDirectory(); + if ($directory instanceof FlexDirectory) { + $serialized['directory'] = [ + 'type' => $directory->getFlexType() + ]; + } + } + + return $serialized; + } + + /** + * @param array|null $data + * @param array $config + * @return void + */ + protected function init(?array $data, array $config): void + { + parent::init($data, $config); + + $data = $data ?? []; + /** @var FlexObjectInterface|null $object */ + $object = $config['object'] ?? null; + $create = true; + if ($object) { + $directory = $object->getFlexDirectory(); + $create = !$object->exists(); + } elseif (null === ($directory = $config['directory'] ?? null)) { + $flex = $config['flex'] ?? static::$flex; + $type = $data['object']['type'] ?? $data['directory']['type'] ?? null; + $directory = $flex && $type ? $flex->getDirectory($type) : null; + } + + if ($directory && $create && isset($data['object']['serialized'])) { + // TODO: update instead of create new. + $object = $directory->createObject($data['object']['serialized'], $data['object']['key'] ?? ''); + } + + if ($object) { + $this->setObject($object); + } elseif ($directory) { + $this->setDirectory($directory); + } + } +} diff --git a/system/src/Grav/Framework/Flex/FlexIndex.php b/system/src/Grav/Framework/Flex/FlexIndex.php new file mode 100644 index 0000000..3ce0a46 --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexIndex.php @@ -0,0 +1,905 @@ + + * @implements FlexIndexInterface + * @mixin C + */ +class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexIndexInterface +{ + const VERSION = 1; + + /** @var FlexDirectory|null */ + private $_flexDirectory; + /** @var string */ + private $_keyField; + /** @var array */ + private $_indexKeys; + + /** + * @param FlexDirectory $directory + * @return static + * @phpstan-return static + */ + public static function createFromStorage(FlexDirectory $directory) + { + return static::createFromArray(static::loadEntriesFromStorage($directory->getStorage()), $directory); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::createFromArray() + */ + public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null) + { + $instance = new static($entries, $directory); + $instance->setKeyField($keyField); + + return $instance; + } + + /** + * @param FlexStorageInterface $storage + * @return array + */ + public static function loadEntriesFromStorage(FlexStorageInterface $storage): array + { + return $storage->getExistingKeys(); + } + + /** + * You can define indexes for fast lookup. + * + * Primary key: $meta['key'] + * Secondary keys: $meta['my_field'] + * + * @param array $meta + * @param array $data + * @param FlexStorageInterface $storage + * @return void + */ + public static function updateObjectMeta(array &$meta, array $data, FlexStorageInterface $storage) + { + // For backwards compatibility, no need to call this method when you override this method. + static::updateIndexData($meta, $data); + } + + /** + * Initializes a new FlexIndex. + * + * @param array $entries + * @param FlexDirectory|null $directory + */ + public function __construct(array $entries = [], FlexDirectory $directory = null) + { + // @phpstan-ignore-next-line + if (get_class($this) === __CLASS__) { + user_error('Using ' . __CLASS__ . ' directly is deprecated since Grav 1.7, use \Grav\Common\Flex\Types\Generic\GenericIndex or your own class instead', E_USER_DEPRECATED); + } + + parent::__construct($entries); + + $this->_flexDirectory = $directory; + $this->setKeyField(null); + } + + /** + * {@inheritdoc} + * @see FlexCommonInterface::hasFlexFeature() + */ + public function hasFlexFeature(string $name): bool + { + return in_array($name, $this->getFlexFeatures(), true); + } + + /** + * {@inheritdoc} + * @see FlexCommonInterface::hasFlexFeature() + */ + public function getFlexFeatures(): array + { + $implements = class_implements($this->getFlexDirectory()->getCollectionClass()); + + $list = []; + foreach ($implements as $interface) { + if ($pos = strrpos($interface, '\\')) { + $interface = substr($interface, $pos+1); + } + + $list[] = Inflector::hyphenize(str_replace('Interface', '', $interface)); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::search() + */ + public function search(string $search, $properties = null, array $options = null) + { + return $this->__call('search', [$search, $properties, $options]); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::sort() + */ + public function sort(array $orderings) + { + return $this->orderBy($orderings); + } + + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::filterBy() + */ + public function filterBy(array $filters) + { + return $this->__call('filterBy', [$filters]); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexType() + */ + public function getFlexType(): string + { + return $this->getFlexDirectory()->getFlexType(); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getFlexDirectory(): FlexDirectory + { + if (null === $this->_flexDirectory) { + throw new RuntimeException('Flex Directory not defined, object is not fully defined'); + } + + return $this->_flexDirectory; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getTimestamp() + */ + public function getTimestamp(): int + { + $timestamps = $this->getTimestamps(); + + return $timestamps ? max($timestamps) : time(); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getCacheKey() + */ + public function getCacheKey(): string + { + return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->getKeys()) . $this->_keyField); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getCacheChecksum() + */ + public function getCacheChecksum(): string + { + $list = []; + foreach ($this->getEntries() as $key => $value) { + $list[$key] = $value['checksum'] ?? $value['storage_timestamp']; + } + + return sha1((string)json_encode($list)); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getTimestamps() + */ + public function getTimestamps(): array + { + return $this->getIndexMap('storage_timestamp'); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getStorageKeys() + */ + public function getStorageKeys(): array + { + return $this->getIndexMap('storage_key'); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexKeys() + */ + public function getFlexKeys(): array + { + // Get storage keys for the objects. + $keys = []; + $type = $this->getFlexDirectory()->getFlexType() . '.obj:'; + + foreach ($this->getEntries() as $key => $value) { + $keys[$key] = $value['flex_key'] ?? $type . $value['storage_key']; + } + + return $keys; + } + + /** + * {@inheritdoc} + * @see FlexIndexInterface::withKeyField() + */ + public function withKeyField(string $keyField = null) + { + $keyField = $keyField ?: 'key'; + if ($keyField === $this->getKeyField()) { + return $this; + } + + $type = $keyField === 'flex_key' ? $this->getFlexDirectory()->getFlexType() . '.obj:' : ''; + $entries = []; + foreach ($this->getEntries() as $key => $value) { + if (!isset($value['key'])) { + $value['key'] = $key; + } + + if (isset($value[$keyField])) { + $entries[$value[$keyField]] = $value; + } elseif ($keyField === 'flex_key') { + $entries[$type . $value['storage_key']] = $value; + } + } + + return $this->createFrom($entries, $keyField); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getIndex() + */ + public function getIndex() + { + return $this; + } + + /** + * @return FlexCollectionInterface + * @phpstan-return C + */ + public function getCollection() + { + return $this->loadCollection(); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::render() + */ + public function render(string $layout = null, array $context = []) + { + return $this->__call('render', [$layout, $context]); + } + + /** + * {@inheritdoc} + * @see FlexIndexInterface::getFlexKeys() + */ + public function getIndexMap(string $indexKey = null) + { + if (null === $indexKey) { + return $this->getEntries(); + } + + // Get storage keys for the objects. + $index = []; + foreach ($this->getEntries() as $key => $value) { + $index[$key] = $value[$indexKey] ?? null; + } + + return $index; + } + + /** + * @param string $key + * @return array + */ + public function getMetaData($key): array + { + return $this->getEntries()[$key] ?? []; + } + + /** + * @return string + */ + public function getKeyField(): string + { + return $this->_keyField ?? 'storage_key'; + } + + /** + * @param string|null $namespace + * @return CacheInterface + */ + public function getCache(string $namespace = null) + { + return $this->getFlexDirectory()->getCache($namespace); + } + + /** + * @param array $orderings + * @return static + * @phpstan-return static + */ + public function orderBy(array $orderings) + { + if (!$orderings || !$this->count()) { + return $this; + } + + // Handle primary key alias. + $keyField = $this->getFlexDirectory()->getStorage()->getKeyField(); + if ($keyField !== 'key' && $keyField !== 'storage_key' && isset($orderings[$keyField])) { + $orderings['key'] = $orderings[$keyField]; + unset($orderings[$keyField]); + } + + // Check if ordering needs to load the objects. + if (array_diff_key($orderings, $this->getIndexKeys())) { + return $this->__call('orderBy', [$orderings]); + } + + // Ordering can be done by using index only. + $previous = null; + foreach (array_reverse($orderings) as $field => $ordering) { + $field = (string)$field; + if ($this->getKeyField() === $field) { + $keys = $this->getKeys(); + $search = array_combine($keys, $keys) ?: []; + } elseif ($field === 'flex_key') { + $search = $this->getFlexKeys(); + } else { + $search = $this->getIndexMap($field); + } + + // Update current search to match the previous ordering. + if (null !== $previous) { + $search = array_replace($previous, $search); + } + + // Order by current field. + if (strtoupper($ordering) === 'DESC') { + arsort($search, SORT_NATURAL | SORT_FLAG_CASE); + } else { + asort($search, SORT_NATURAL | SORT_FLAG_CASE); + } + + $previous = $search; + } + + return $this->createFrom(array_replace($previous ?? [], $this->getEntries()) ?? []); + } + + /** + * {@inheritDoc} + */ + public function call($method, array $arguments = []) + { + return $this->__call('call', [$method, $arguments]); + } + + /** + * @param string $name + * @param array $arguments + * @return mixed + */ + public function __call($name, $arguments) + { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + + /** @var FlexCollection $className */ + $className = $this->getFlexDirectory()->getCollectionClass(); + $cachedMethods = $className::getCachedMethods(); + + $flexType = $this->getFlexType(); + + if (!empty($cachedMethods[$name])) { + $type = $cachedMethods[$name]; + if ($type === 'session') { + /** @var Session $session */ + $session = Grav::instance()['session']; + $cacheKey = $session->getId() . ($session->user->username ?? ''); + } else { + $cacheKey = ''; + } + $key = "{$flexType}.idx." . sha1($name . '.' . $cacheKey . json_encode($arguments) . $this->getCacheKey()); + $checksum = $this->getCacheChecksum(); + + $cache = $this->getCache('object'); + + try { + $cached = $cache->get($key); + $test = $cached[0] ?? null; + $result = $test === $checksum ? ($cached[1] ?? null) : null; + + // Make sure the keys aren't changed if the returned type is the same index type. + if ($result instanceof self && $flexType === $result->getFlexType()) { + $result = $result->withKeyField($this->getKeyField()); + } + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + } + + if (!isset($result)) { + $collection = $this->loadCollection(); + $result = $collection->{$name}(...$arguments); + $debugger->addMessage("Cache miss: '{$flexType}::{$name}()'", 'debug'); + + try { + // If flex collection is returned, convert it back to flex index. + if ($result instanceof FlexCollection) { + $cached = $result->getFlexDirectory()->getIndex($result->getKeys(), $this->getKeyField()); + } else { + $cached = $result; + } + + $cache->set($key, [$checksum, $cached]); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + + // TODO: log error. + } + } + } else { + $collection = $this->loadCollection(); + $result = $collection->{$name}(...$arguments); + if (!isset($cachedMethods[$name])) { + $debugger->addMessage("Call '{$flexType}:{$name}()' isn't cached", 'debug'); + } + } + + return $result; + } + + /** + * @return array + */ + public function __serialize(): array + { + return ['type' => $this->getFlexType(), 'entries' => $this->getEntries()]; + } + + /** + * @param array $data + * @return void + */ + public function __unserialize(array $data): void + { + $this->_flexDirectory = Grav::instance()['flex']->getDirectory($data['type']); + $this->setEntries($data['entries']); + } + + /** + * @return array + */ + public function __debugInfo() + { + return [ + 'type:private' => $this->getFlexType(), + 'key:private' => $this->getKey(), + 'entries_key:private' => $this->getKeyField(), + 'entries:private' => $this->getEntries() + ]; + } + + /** + * @param array $entries + * @param string|null $keyField + * @return static + * @phpstan-return static + */ + protected function createFrom(array $entries, string $keyField = null) + { + /** @phpstan-var static $index */ + $index = new static($entries, $this->getFlexDirectory()); + $index->setKeyField($keyField ?? $this->_keyField); + + return $index; + } + + /** + * @param string|null $keyField + * @return void + */ + protected function setKeyField(string $keyField = null) + { + $this->_keyField = $keyField ?? 'storage_key'; + } + + /** + * @return array + */ + protected function getIndexKeys() + { + if (null === $this->_indexKeys) { + $entries = $this->getEntries(); + $first = reset($entries); + if ($first) { + $keys = array_keys($first); + $keys = array_combine($keys, $keys) ?: []; + } else { + $keys = []; + } + + $this->setIndexKeys($keys); + } + + return $this->_indexKeys; + } + + /** + * @param array $indexKeys + * @return void + */ + protected function setIndexKeys(array $indexKeys) + { + // Add defaults. + $indexKeys += [ + 'key' => 'key', + 'storage_key' => 'storage_key', + 'storage_timestamp' => 'storage_timestamp', + 'flex_key' => 'flex_key' + ]; + + + $this->_indexKeys = $indexKeys; + } + + /** + * @return string + */ + protected function getTypePrefix() + { + return 'i.'; + } + + /** + * @param string $key + * @param mixed $value + * @return ObjectInterface|null + */ + protected function loadElement($key, $value): ?ObjectInterface + { + $objects = $this->getFlexDirectory()->loadObjects([$key => $value]); + + return $objects ? reset($objects): null; + } + + /** + * @param array|null $entries + * @return ObjectInterface[] + */ + protected function loadElements(array $entries = null): array + { + return $this->getFlexDirectory()->loadObjects($entries ?? $this->getEntries()); + } + + /** + * @param array|null $entries + * @return CollectionInterface + * @phpstan-return C + */ + protected function loadCollection(array $entries = null): CollectionInterface + { + /** @var C $collection */ + $collection = $this->getFlexDirectory()->loadCollection($entries ?? $this->getEntries(), $this->_keyField); + + return $collection; + } + + /** + * @param mixed $value + * @return bool + */ + protected function isAllowedElement($value): bool + { + return $value instanceof FlexObject; + } + + /** + * @param FlexObjectInterface $object + * @return mixed + */ + protected function getElementMeta($object) + { + return $object->getMetaData(); + } + + /** + * @param FlexObjectInterface $element + * @return string + */ + protected function getCurrentKey($element) + { + $keyField = $this->getKeyField(); + if ($keyField === 'storage_key') { + return $element->getStorageKey(); + } + if ($keyField === 'flex_key') { + return $element->getFlexKey(); + } + if ($keyField === 'key') { + return $element->getKey(); + } + + return $element->getKey(); + } + + /** + * @param FlexStorageInterface $storage + * @param array $index Saved index + * @param array $entries Updated index + * @param array $options + * @return array Compiled list of entries + */ + protected static function updateIndexFile(FlexStorageInterface $storage, array $index, array $entries, array $options = []): array + { + $indexFile = static::getIndexFile($storage); + if (null === $indexFile) { + return $entries; + } + + // Calculate removed objects. + $removed = array_diff_key($index, $entries); + + // First get rid of all removed objects. + if ($removed) { + $index = array_diff_key($index, $removed); + } + + if ($entries && empty($options['force_update'])) { + // Calculate difference between saved index and current data. + foreach ($index as $key => $entry) { + $storage_key = $entry['storage_key'] ?? null; + if (isset($entries[$storage_key]) && $entries[$storage_key]['storage_timestamp'] === $entry['storage_timestamp']) { + // Entry is up to date, no update needed. + unset($entries[$storage_key]); + } + } + + if (empty($entries) && empty($removed)) { + // No objects were added, updated or removed. + return $index; + } + } elseif (!$removed) { + // There are no objects and nothing was removed. + return []; + } + + // Index should be updated, lock the index file for saving. + $indexFile->lock(); + + // Read all the data rows into an array using chunks of 100. + $keys = array_fill_keys(array_keys($entries), null); + $chunks = array_chunk($keys, 100, true); + $updated = $added = []; + foreach ($chunks as $keys) { + $rows = $storage->readRows($keys); + + $keyField = $storage->getKeyField(); + + // Go through all the updated objects and refresh their index data. + foreach ($rows as $key => $row) { + if (null !== $row || !empty($options['include_missing'])) { + $entry = $entries[$key] + ['key' => $key]; + if ($keyField !== 'storage_key' && isset($row[$keyField])) { + $entry['key'] = $row[$keyField]; + } + static::updateObjectMeta($entry, $row ?? [], $storage); + if (isset($row['__ERROR'])) { + $entry['__ERROR'] = true; + static::onException(new RuntimeException(sprintf('Object failed to load: %s (%s)', $key, + $row['__ERROR']))); + } + if (isset($index[$key])) { + // Update object in the index. + $updated[$key] = $entry; + } else { + // Add object into the index. + $added[$key] = $entry; + } + + // Either way, update the entry. + $index[$key] = $entry; + } elseif (isset($index[$key])) { + // Remove object from the index. + $removed[$key] = $index[$key]; + unset($index[$key]); + } + } + unset($rows); + } + + // Sort the index before saving it. + ksort($index, SORT_NATURAL | SORT_FLAG_CASE); + + static::onChanges($index, $added, $updated, $removed); + + $indexFile->save(['version' => static::VERSION, 'timestamp' => time(), 'count' => count($index), 'index' => $index]); + $indexFile->unlock(); + + return $index; + } + + /** + * @param array $entry + * @param array $data + * @return void + * @deprecated 1.7 Use static ::updateObjectMeta() method instead. + */ + protected static function updateIndexData(array &$entry, array $data) + { + } + + /** + * @param FlexStorageInterface $storage + * @return array + */ + protected static function loadIndex(FlexStorageInterface $storage) + { + $indexFile = static::getIndexFile($storage); + + if ($indexFile) { + $data = []; + try { + $data = (array)$indexFile->content(); + $version = $data['version'] ?? null; + if ($version !== static::VERSION) { + $data = []; + } + } catch (Exception $e) { + $e = new RuntimeException(sprintf('Index failed to load: %s', $e->getMessage()), $e->getCode(), $e); + + static::onException($e); + } + + if ($data) { + return $data; + } + } + + return ['version' => static::VERSION, 'timestamp' => 0, 'count' => 0, 'index' => []]; + } + + /** + * @param FlexStorageInterface $storage + * @return array + */ + protected static function loadEntriesFromIndex(FlexStorageInterface $storage) + { + $data = static::loadIndex($storage); + + return $data['index'] ?? []; + } + + /** + * @param FlexStorageInterface $storage + * @return CompiledYamlFile|null + */ + protected static function getIndexFile(FlexStorageInterface $storage) + { + if (!method_exists($storage, 'isIndexed') || !$storage->isIndexed()) { + return null; + } + + $path = $storage->getStoragePath(); + if (!$path) { + return null; + } + + // Load saved index file. + $grav = Grav::instance(); + $locator = $grav['locator']; + $filename = $locator->findResource("{$path}/index.yaml", true, true); + + return CompiledYamlFile::instance($filename); + } + + /** + * @param Exception $e + * @return void + */ + protected static function onException(Exception $e) + { + $grav = Grav::instance(); + + /** @var Logger $logger */ + $logger = $grav['log']; + $logger->addAlert($e->getMessage()); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addException($e); + $debugger->addMessage($e, 'error'); + } + + /** + * @param array $entries + * @param array $added + * @param array $updated + * @param array $removed + * @return void + */ + protected static function onChanges(array $entries, array $added, array $updated, array $removed) + { + $addedCount = count($added); + $updatedCount = count($updated); + $removedCount = count($removed); + + if ($addedCount + $updatedCount + $removedCount) { + $message = sprintf('Index updated, %d objects (%d added, %d updated, %d removed).', count($entries), $addedCount, $updatedCount, $removedCount); + + $grav = Grav::instance(); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addMessage($message, 'debug'); + } + } + + // DEPRECATED METHODS + + /** + * @param bool $prefix + * @return string + * @deprecated 1.6 Use `->getFlexType()` instead. + */ + public function getType($prefix = false) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED); + + $type = $prefix ? $this->getTypePrefix() : ''; + + return $type . $this->getFlexType(); + } +} diff --git a/system/src/Grav/Framework/Flex/FlexObject.php b/system/src/Grav/Framework/Flex/FlexObject.php new file mode 100644 index 0000000..ac51b34 --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexObject.php @@ -0,0 +1,1290 @@ + true, + 'getType' => true, + 'getFlexType' => true, + 'getFlexDirectory' => true, + 'hasFlexFeature' => true, + 'getFlexFeatures' => true, + 'getCacheKey' => true, + 'getCacheChecksum' => false, + 'getTimestamp' => true, + 'value' => true, + 'exists' => true, + 'hasProperty' => true, + 'getProperty' => true, + + // FlexAclTrait + 'isAuthorized' => 'session', + ]; + } + + /** + * @param array $elements + * @param array $storage + * @param FlexDirectory $directory + * @param bool $validate + * @return static + */ + public static function createFromStorage(array $elements, array $storage, FlexDirectory $directory, bool $validate = false) + { + $instance = new static($elements, $storage['key'], $directory, $validate); + $instance->setMetaData($storage); + + return $instance; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::__construct() + */ + public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false) + { + if (get_class($this) === __CLASS__) { + user_error('Using ' . __CLASS__ . ' directly is deprecated since Grav 1.7, use \Grav\Common\Flex\Types\Generic\GenericObject or your own class instead', E_USER_DEPRECATED); + } + + $this->_flexDirectory = $directory; + + if (isset($elements['__META'])) { + $this->setMetaData($elements['__META']); + unset($elements['__META']); + } + + if ($validate) { + $blueprint = $this->getFlexDirectory()->getBlueprint(); + + $blueprint->validate($elements, ['xss_check' => false]); + + $elements = $blueprint->filter($elements, true, true); + } + + $this->filterElements($elements); + + $this->objectConstruct($elements, $key); + } + + /** + * {@inheritdoc} + * @see FlexCommonInterface::hasFlexFeature() + */ + public function hasFlexFeature(string $name): bool + { + return in_array($name, $this->getFlexFeatures(), true); + } + + /** + * {@inheritdoc} + * @see FlexCommonInterface::hasFlexFeature() + */ + public function getFlexFeatures(): array + { + $implements = class_implements($this); + + $list = []; + foreach ($implements as $interface) { + if ($pos = strrpos($interface, '\\')) { + $interface = substr($interface, $pos+1); + } + + $list[] = Inflector::hyphenize(str_replace('Interface', '', $interface)); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getFlexType() + */ + public function getFlexType(): string + { + return $this->_flexDirectory->getFlexType(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getFlexDirectory() + */ + public function getFlexDirectory(): FlexDirectory + { + return $this->_flexDirectory; + } + + /** + * Refresh object from the storage. + * + * @param bool $keepMissing + * @return bool True if the object was refreshed + */ + public function refresh(bool $keepMissing = false): bool + { + $key = $this->getStorageKey(); + if ('' === $key) { + return false; + } + + $storage = $this->getFlexDirectory()->getStorage(); + $meta = $storage->getMetaData([$key])[$key] ?? null; + + $newChecksum = $meta['checksum'] ?? $meta['storage_timestamp'] ?? null; + $curChecksum = $this->_meta['checksum'] ?? $this->_meta['storage_timestamp'] ?? null; + + // Check if object is up to date with the storage. + if (null === $newChecksum || $newChecksum === $curChecksum) { + return false; + } + + // Get current elements (if requested). + $current = $keepMissing ? $this->getElements() : []; + // Get elements from the filesystem. + $elements = $storage->readRows([$key => null])[$key] ?? null; + if (null !== $elements) { + $meta = $elements['__META'] ?? $meta; + unset($elements['__META']); + $this->filterElements($elements); + $newKey = $meta['key'] ?? $this->getKey(); + if ($meta) { + $this->setMetaData($meta); + } + $this->objectConstruct($elements, $newKey); + + if ($current) { + // Inject back elements which are missing in the filesystem. + $data = $this->getBlueprint()->flattenData($current); + foreach ($data as $property => $value) { + if (strpos($property, '.') === false) { + $this->defProperty($property, $value); + } else { + $this->defNestedProperty($property, $value); + } + } + } + + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addMessage("Refreshed {$this->getFlexType()} object {$this->getKey()}", 'debug'); + } + + return true; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getTimestamp() + */ + public function getTimestamp(): int + { + return $this->_meta['storage_timestamp'] ?? 0; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getCacheKey() + */ + public function getCacheKey(): string + { + return $this->hasKey() ? $this->getTypePrefix() . $this->getFlexType() . '.' . $this->getKey() : ''; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getCacheChecksum() + */ + public function getCacheChecksum(): string + { + return (string)($this->_meta['checksum'] ?? $this->getTimestamp()); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::search() + */ + public function search(string $search, $properties = null, array $options = null): float + { + $properties = (array)($properties ?? $this->getFlexDirectory()->getConfig('data.search.fields')); + if (!$properties) { + $fields = $this->getFlexDirectory()->getConfig('admin.views.list.fields') ?? $this->getFlexDirectory()->getConfig('admin.list.fields', []); + foreach ($fields as $property => $value) { + if (!empty($value['link'])) { + $properties[] = $property; + } + } + } + + $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options'); + + $weight = 0; + foreach ($properties as $property) { + if (strpos($property, '.')) { + $weight += $this->searchNestedProperty($property, $search, $options); + } else { + $weight += $this->searchProperty($property, $search, $options); + } + } + + return $weight > 0 ? min($weight, 1) : 0; + } + + /** + * {@inheritdoc} + * @see ObjectInterface::getFlexKey() + */ + public function getKey() + { + return (string)$this->_key; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getFlexKey() + */ + public function getFlexKey(): string + { + $key = $this->_meta['flex_key'] ?? null; + + if (!$key && $key = $this->getStorageKey()) { + $key = $this->_flexDirectory->getFlexType() . '.obj:' . $key; + } + + return (string)$key; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getStorageKey() + */ + public function getStorageKey(): string + { + return (string)($this->storage_key ?? $this->_meta['storage_key'] ?? null); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getMetaData() + */ + public function getMetaData(): array + { + return $this->_meta ?? []; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::exists() + */ + public function exists(): bool + { + $key = $this->getStorageKey(); + + return $key && $this->getFlexDirectory()->getStorage()->hasKey($key); + } + + /** + * @param string $property + * @param string $search + * @param array|null $options + * @return float + */ + public function searchProperty(string $property, string $search, array $options = null): float + { + $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options'); + $value = $this->getProperty($property); + + return $this->searchValue($property, $value, $search, $options); + } + + /** + * @param string $property + * @param string $search + * @param array|null $options + * @return float + */ + public function searchNestedProperty(string $property, string $search, array $options = null): float + { + $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options'); + if ($property === 'key') { + $value = $this->getKey(); + } else { + $value = $this->getNestedProperty($property); + } + + return $this->searchValue($property, $value, $search, $options); + } + + /** + * @param string $name + * @param mixed $value + * @param string $search + * @param array|null $options + * @return float + */ + protected function searchValue(string $name, $value, string $search, array $options = null): float + { + $options = $options ?? []; + + // Ignore empty search strings. + $search = trim($search); + if ($search === '') { + return 0; + } + + // Search only non-empty string values. + if (!is_string($value) || $value === '') { + return 0; + } + + $caseSensitive = $options['case_sensitive'] ?? false; + + $tested = false; + if (($tested |= !empty($options['same_as']))) { + if ($caseSensitive) { + if ($value === $search) { + return (float)$options['same_as']; + } + } elseif (mb_strtolower($value) === mb_strtolower($search)) { + return (float)$options['same_as']; + } + } + if (($tested |= !empty($options['starts_with'])) && Utils::startsWith($value, $search, $caseSensitive)) { + return (float)$options['starts_with']; + } + if (($tested |= !empty($options['ends_with'])) && Utils::endsWith($value, $search, $caseSensitive)) { + return (float)$options['ends_with']; + } + if ((!$tested || !empty($options['contains'])) && Utils::contains($value, $search, $caseSensitive)) { + return (float)($options['contains'] ?? 1); + } + + return 0; + } + + /** + * Get original data before update + * + * @return array + */ + public function getOriginalData(): array + { + return $this->_original ?? []; + } + + /** + * Get diff array from the object. + * + * @return array + */ + public function getDiff(): array + { + $blueprint = $this->getBlueprint(); + + $flattenOriginal = $blueprint->flattenData($this->getOriginalData()); + $flattenElements = $blueprint->flattenData($this->getElements()); + $removedElements = array_diff_key($flattenOriginal, $flattenElements); + $diff = []; + + // Include all added or changed keys. + foreach ($flattenElements as $key => $value) { + $orig = $flattenOriginal[$key] ?? null; + if ($orig !== $value) { + $diff[$key] = ['old' => $orig, 'new' => $value]; + } + } + + // Include all removed keys. + foreach ($removedElements as $key => $value) { + $diff[$key] = ['old' => $value, 'new' => null]; + } + + return $diff; + } + + /** + * Get any changes from the object. + * + * @return array + */ + public function getChanges(): array + { + $diff = $this->getDiff(); + + $data = new Data(); + foreach ($diff as $key => $change) { + $data->set($key, $change['new']); + } + + return $data->toArray(); + } + + /** + * @return string + */ + protected function getTypePrefix(): string + { + return 'o.'; + } + + /** + * Alias of getBlueprint() + * + * @return Blueprint + * @deprecated 1.6 Admin compatibility + */ + public function blueprints() + { + return $this->getBlueprint(); + } + + /** + * @param string|null $namespace + * @return CacheInterface + */ + public function getCache(string $namespace = null) + { + return $this->_flexDirectory->getCache($namespace); + } + + /** + * @param string|null $key + * @return $this + */ + public function setStorageKey($key = null) + { + $this->storage_key = $key ?? ''; + + return $this; + } + + /** + * @param int $timestamp + * @return $this + */ + public function setTimestamp($timestamp = null) + { + $this->storage_timestamp = $timestamp ?? time(); + + return $this; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::render() + */ + public function render(string $layout = null, array $context = []) + { + if (!$layout) { + $config = $this->getTemplateConfig(); + $layout = $config['object']['defaults']['layout'] ?? 'default'; + } + + $type = $this->getFlexType(); + + $grav = Grav::instance(); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->startTimer('flex-object-' . ($debugKey = uniqid($type, false)), 'Render Object ' . $type . ' (' . $layout . ')'); + + $key = $this->getCacheKey(); + + // Disable caching if context isn't all scalars. + if ($key) { + foreach ($context as $value) { + if (!is_scalar($value)) { + $key = ''; + break; + } + } + } + + if ($key) { + // Create a new key which includes layout and context. + $key = md5($key . '.' . $layout . json_encode($context)); + $cache = $this->getCache('render'); + } else { + $cache = null; + } + + try { + $data = $cache ? $cache->get($key) : null; + + $block = $data ? HtmlBlock::fromArray($data) : null; + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + + $block = null; + } catch (\InvalidArgumentException $e) { + $debugger->addException($e); + + $block = null; + } + + $checksum = $this->getCacheChecksum(); + if ($block && $checksum !== $block->getChecksum()) { + $block = null; + } + + if (!$block) { + $block = HtmlBlock::create($key ?: null); + $block->setChecksum($checksum); + if (!$cache) { + $block->disableCache(); + } + + $event = new Event([ + 'type' => 'flex', + 'directory' => $this->getFlexDirectory(), + 'object' => $this, + 'layout' => &$layout, + 'context' => &$context + ]); + $this->triggerEvent('onRender', $event); + + $output = $this->getTemplate($layout)->render( + [ + 'grav' => $grav, + 'config' => $grav['config'], + 'block' => $block, + 'directory' => $this->getFlexDirectory(), + 'object' => $this, + 'layout' => $layout + ] + $context + ); + + if ($debugger->enabled()) { + $name = $this->getKey() . ' (' . $type . ')'; + $output = "\n\n{$output}\n\n"; + } + + $block->setContent($output); + + try { + $cache && $block->isCached() && $cache->set($key, $block->toArray()); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + } + } + + $debugger->stopTimer('flex-object-' . $debugKey); + + return $block; + } + + /** + * @return array + */ + public function jsonSerialize() + { + return $this->getElements(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::prepareStorage() + */ + public function prepareStorage(): array + { + return ['__META' => $this->getMetaData()] + $this->getElements(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::update() + */ + public function update(array $data, array $files = []) + { + if ($data) { + // Get currently stored data. + $elements = $this->getElements(); + + // Store original version of the object. + if ($this->_original === null) { + $this->_original = $elements; + } + + $blueprint = $this->getBlueprint(); + + // Process updated data through the object filters. + $this->filterElements($data); + + // Merge existing object to the test data to be validated. + $test = $blueprint->mergeData($elements, $data); + + // Validate and filter elements and throw an error if any issues were found. + $blueprint->validate($test + ['storage_key' => $this->getStorageKey(), 'timestamp' => $this->getTimestamp()], ['xss_check' => false]); + $data = $blueprint->filter($data, true, true); + + // Finally update the object. + $flattenData = $blueprint->flattenData($data); + foreach ($flattenData as $key => $value) { + if ($value === null) { + $this->unsetNestedProperty($key); + } else { + $this->setNestedProperty($key, $value); + } + } + } + + if ($files && method_exists($this, 'setUpdatedMedia')) { + $this->setUpdatedMedia($files); + } + + return $this; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::create() + */ + public function create(string $key = null) + { + if ($key) { + $this->setStorageKey($key); + } + + if ($this->exists()) { + throw new RuntimeException('Cannot create new object (Already exists)'); + } + + return $this->save(); + } + + /** + * @param string|null $key + * @return FlexObject|FlexObjectInterface + */ + public function createCopy(string $key = null) + { + $this->markAsCopy(); + + return $this->create($key); + } + + /** + * @param UserInterface|null $user + */ + public function check(UserInterface $user = null): void + { + // If user has been provided, check if the user has permissions to save this object. + if ($user && !$this->isAuthorized('save', null, $user)) { + throw new \RuntimeException('Forbidden', 403); + } + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::save() + */ + public function save() + { + $this->triggerEvent('onBeforeSave'); + + $storage = $this->getFlexDirectory()->getStorage(); + + $storageKey = $this->getStorageKey() ?: '@@' . spl_object_hash($this); + + $result = $storage->replaceRows([$storageKey => $this->prepareStorage()]); + + if (method_exists($this, 'clearMediaCache')) { + $this->clearMediaCache(); + } + + $value = reset($result); + $meta = $value['__META'] ?? null; + if ($meta) { + /** @var FlexIndex $indexClass */ + $indexClass = $this->getFlexDirectory()->getIndexClass(); + $indexClass::updateObjectMeta($meta, $value, $storage); + $this->_meta = $meta; + } + + if ($value) { + $storageKey = $meta['storage_key'] ?? (string)key($result); + if ($storageKey !== '') { + $this->setStorageKey($storageKey); + } + + $newKey = $meta['key'] ?? ($this->hasKey() ? $this->getKey() : null); + $this->setKey($newKey ?? $storageKey); + } + + // FIXME: For some reason locator caching isn't cleared for the file, investigate! + $locator = Grav::instance()['locator']; + $locator->clearCache(); + + if (method_exists($this, 'saveUpdatedMedia')) { + $this->saveUpdatedMedia(); + } + + try { + $this->getFlexDirectory()->reloadIndex(); + if (method_exists($this, 'clearMediaCache')) { + $this->clearMediaCache(); + } + } catch (Exception $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + // Caching failed, but we can ignore that for now. + } + + $this->triggerEvent('onAfterSave'); + + return $this; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::delete() + */ + public function delete() + { + if (!$this->exists()) { + return $this; + } + + $this->triggerEvent('onBeforeDelete'); + + $this->getFlexDirectory()->getStorage()->deleteRows([$this->getStorageKey() => $this->prepareStorage()]); + + try { + $this->getFlexDirectory()->reloadIndex(); + if (method_exists($this, 'clearMediaCache')) { + $this->clearMediaCache(); + } + } catch (Exception $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + // Caching failed, but we can ignore that for now. + } + + $this->triggerEvent('onAfterDelete'); + + return $this; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getBlueprint() + */ + public function getBlueprint(string $name = '') + { + if (!isset($this->_blueprint[$name])) { + $blueprint = $this->doGetBlueprint($name); + $blueprint->setScope('object'); + $blueprint->setObject($this); + + $this->_blueprint[$name] = $blueprint->init(); + } + + return $this->_blueprint[$name]; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getForm() + */ + public function getForm(string $name = '', array $options = null) + { + $hash = $name . '-' . md5(json_encode($options, JSON_THROW_ON_ERROR)); + if (!isset($this->_forms[$hash])) { + $this->_forms[$hash] = $this->createFormObject($name, $options); + } + + return $this->_forms[$hash]; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getDefaultValue() + */ + public function getDefaultValue(string $name, string $separator = null) + { + $separator = $separator ?: '.'; + $path = explode($separator, $name) ?: []; + $offset = array_shift($path) ?? ''; + + $current = $this->getDefaultValues(); + + if (!isset($current[$offset])) { + return null; + } + + $current = $current[$offset]; + + while ($path) { + $offset = array_shift($path); + + if ((is_array($current) || $current instanceof ArrayAccess) && isset($current[$offset])) { + $current = $current[$offset]; + } elseif (is_object($current) && isset($current->{$offset})) { + $current = $current->{$offset}; + } else { + return null; + } + }; + + return $current; + } + + /** + * @return array + */ + public function getDefaultValues(): array + { + return $this->getBlueprint()->getDefaults(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getFormValue() + */ + public function getFormValue(string $name, $default = null, string $separator = null) + { + if ($name === 'storage_key') { + return $this->getStorageKey(); + } + if ($name === 'storage_timestamp') { + return $this->getTimestamp(); + } + + return $this->getNestedProperty($name, $default, $separator); + } + + /** + * @param FlexDirectory $directory + */ + public function setFlexDirectory(FlexDirectory $directory): void + { + $this->_flexDirectory = $directory; + } + + /** + * Returns a string representation of this object. + * + * @return string + */ + public function __toString() + { + return $this->getFlexKey(); + } + + /** + * @return array + */ + public function __debugInfo() + { + return [ + 'type:private' => $this->getFlexType(), + 'storage_key:protected' => $this->getStorageKey(), + 'storage_timestamp:protected' => $this->getTimestamp(), + 'key:private' => $this->getKey(), + 'elements:private' => $this->getElements(), + 'storage:private' => $this->getMetaData() + ]; + } + + /** + * Clone object. + */ + public function __clone() + { + // Allows future compatibility as parent::__clone() works. + } + + protected function markAsCopy(): void + { + $meta = $this->getMetaData(); + $meta['copy'] = true; + $this->_meta = $meta; + } + + /** + * @param string $name + * @return Blueprint + */ + protected function doGetBlueprint(string $name = ''): Blueprint + { + return $this->_flexDirectory->getBlueprint($name ? '.' . $name : $name); + } + + /** + * @param array $meta + */ + protected function setMetaData(array $meta): void + { + $this->_meta = $meta; + } + + /** + * @return array + */ + protected function doSerialize(): array + { + return [ + 'type' => $this->getFlexType(), + 'key' => $this->getKey(), + 'elements' => $this->getElements(), + 'storage' => $this->getMetaData() + ]; + } + + /** + * @param array $serialized + * @param FlexDirectory|null $directory + * @return void + */ + protected function doUnserialize(array $serialized, FlexDirectory $directory = null): void + { + $type = $serialized['type'] ?? 'unknown'; + + if (!isset($serialized['key'], $serialized['type'], $serialized['elements'])) { + throw new \InvalidArgumentException("Cannot unserialize '{$type}': Bad data"); + } + + if (null === $directory) { + $directory = $this->getFlexContainer()->getDirectory($type); + if (!$directory) { + throw new \InvalidArgumentException("Cannot unserialize Flex type '{$type}': Directory not found"); + } + } + + $this->setFlexDirectory($directory); + $this->setMetaData($serialized['storage']); + $this->setKey($serialized['key']); + $this->setElements($serialized['elements']); + } + + /** + * @return array + */ + protected function getTemplateConfig() + { + $config = $this->getFlexDirectory()->getConfig('site.templates', []); + $defaults = array_replace($config['defaults'] ?? [], $config['object']['defaults'] ?? []); + $config['object']['defaults'] = $defaults; + + return $config; + } + + /** + * @param string $layout + * @return array + */ + protected function getTemplatePaths(string $layout): array + { + $config = $this->getTemplateConfig(); + $type = $this->getFlexType(); + $defaults = $config['object']['defaults'] ?? []; + + $ext = $defaults['ext'] ?? '.html.twig'; + $types = array_unique(array_merge([$type], (array)($defaults['type'] ?? null))); + $paths = $config['object']['paths'] ?? [ + 'flex/{TYPE}/object/{LAYOUT}{EXT}', + 'flex-objects/layouts/{TYPE}/object/{LAYOUT}{EXT}' + ]; + $table = ['TYPE' => '%1$s', 'LAYOUT' => '%2$s', 'EXT' => '%3$s']; + + $lookups = []; + foreach ($paths as $path) { + $path = Utils::simpleTemplate($path, $table); + foreach ($types as $type) { + $lookups[] = sprintf($path, $type, $layout, $ext); + } + } + + return array_unique($lookups); + } + + /** + * Filter data coming to constructor or $this->update() request. + * + * NOTE: The incoming data can be an arbitrary array so do not assume anything from its content. + * + * @param array $elements + */ + protected function filterElements(array &$elements): void + { + if (isset($elements['storage_key'])) { + $elements['storage_key'] = trim($elements['storage_key']); + } + if (isset($elements['storage_timestamp'])) { + $elements['storage_timestamp'] = (int)$elements['storage_timestamp']; + } + + unset($elements['_post_entries_save']); + } + + /** + * This methods allows you to override form objects in child classes. + * + * @param string $name Form name + * @param array|null $options Form optiosn + * @return FlexFormInterface + */ + protected function createFormObject(string $name, array $options = null) + { + return new FlexForm($name, $this, $options); + } + + /** + * @param string $action + * @return string + */ + protected function getAuthorizeAction(string $action): string + { + // Handle special action save, which can mean either update or create. + if ($action === 'save') { + $action = $this->exists() ? 'update' : 'create'; + } + + return $action; + } + + /** + * Method to reset blueprints if the type changes. + * + * @return void + * @since 1.7.18 + */ + protected function resetBlueprints(): void + { + $this->_blueprint = []; + } + + // DEPRECATED METHODS + + /** + * @param bool $prefix + * @return string + * @deprecated 1.6 Use `->getFlexType()` instead. + */ + public function getType($prefix = false) + { + user_error(__METHOD__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED); + + $type = $prefix ? $this->getTypePrefix() : ''; + + return $type . $this->getFlexType(); + } + + /** + * @param string $name + * @param mixed|null $default + * @param string|null $separator + * @return mixed + * + * @deprecated 1.6 Use ->getFormValue() method instead. + */ + public function value($name, $default = null, $separator = null) + { + user_error(__METHOD__ . '() is deprecated since Grav 1.6, use ->getFormValue() method instead', E_USER_DEPRECATED); + + return $this->getFormValue($name, $default, $separator); + } + + /** + * @param string $name + * @param object|null $event + * @return $this + * @deprecated 1.7 Moved to \Grav\Common\Flex\Traits\FlexObjectTrait + */ + public function triggerEvent(string $name, $event = null) + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\FlexObjectTrait', E_USER_DEPRECATED); + + if (null === $event) { + $event = new Event([ + 'type' => 'flex', + 'directory' => $this->getFlexDirectory(), + 'object' => $this + ]); + } + if (strpos($name, 'onFlexObject') !== 0 && strpos($name, 'on') === 0) { + $name = 'onFlexObject' . substr($name, 2); + } + + $grav = Grav::instance(); + if ($event instanceof Event) { + $grav->fireEvent($name, $event); + } else { + $grav->dispatchEvent($event); + } + + return $this; + } + + /** + * @param array $storage + * @deprecated 1.7 Use `->setMetaData()` instead. + */ + protected function setStorage(array $storage): void + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, use ->setMetaData() method instead', E_USER_DEPRECATED); + + $this->setMetaData($storage); + } + + /** + * @return array + * @deprecated 1.7 Use `->getMetaData()` instead. + */ + protected function getStorage(): array + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, use ->getMetaData() method instead', E_USER_DEPRECATED); + + return $this->getMetaData(); + } + + /** + * @param string $layout + * @return Template|TemplateWrapper + * @throws LoaderError + * @throws SyntaxError + * @deprecated 1.7 Moved to \Grav\Common\Flex\Traits\GravTrait + */ + protected function getTemplate($layout) + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\GravTrait', E_USER_DEPRECATED); + + $grav = Grav::instance(); + + /** @var Twig $twig */ + $twig = $grav['twig']; + + try { + return $twig->twig()->resolveTemplate($this->getTemplatePaths($layout)); + } catch (LoaderError $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + return $twig->twig()->resolveTemplate(['flex/404.html.twig']); + } + } + + /** + * @return Flex + * @deprecated 1.7 Moved to \Grav\Common\Flex\Traits\GravTrait + */ + protected function getFlexContainer(): Flex + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\GravTrait', E_USER_DEPRECATED); + + /** @var Flex $flex */ + $flex = Grav::instance()['flex']; + + return $flex; + } + + /** + * @return UserInterface|null + * @deprecated 1.7 Moved to \Grav\Common\Flex\Traits\GravTrait + */ + protected function getActiveUser(): ?UserInterface + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\GravTrait', E_USER_DEPRECATED); + + /** @var UserInterface|null $user */ + $user = Grav::instance()['user'] ?? null; + + return $user; + } + + /** + * @return string + * @deprecated 1.7 Moved to \Grav\Common\Flex\Traits\GravTrait + */ + protected function getAuthorizeScope(): string + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\GravTrait', E_USER_DEPRECATED); + + return isset(Grav::instance()['admin']) ? 'admin' : 'site'; + } +} diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexAuthorizeInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexAuthorizeInterface.php new file mode 100644 index 0000000..102dbf4 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexAuthorizeInterface.php @@ -0,0 +1,33 @@ + + */ +interface FlexCollectionInterface extends FlexCommonInterface, ObjectCollectionInterface, NestedObjectInterface +{ + /** + * Creates a Flex Collection from an array. + * + * @used-by FlexDirectory::createCollection() Official method to create a Flex Collection. + * + * @param FlexObjectInterface[] $entries Associated array of Flex Objects to be included in the collection. + * @param FlexDirectory $directory Flex Directory where all the objects belong into. + * @param string|null $keyField Key field used to index the collection. + * @return static Returns a new Flex Collection. + */ + public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null); + + /** + * Creates a new Flex Collection. + * + * @used-by FlexDirectory::createCollection() Official method to create Flex Collection. + * + * @param FlexObjectInterface[] $entries Associated array of Flex Objects to be included in the collection. + * @param FlexDirectory|null $directory Flex Directory where all the objects belong into. + * @throws InvalidArgumentException + */ + public function __construct(array $entries = [], FlexDirectory $directory = null); + + /** + * Search a string from the collection. + * + * @param string $search Search string. + * @param string|string[]|null $properties Properties to search for, defaults to configured properties. + * @param array|null $options Search options, defaults to configured options. + * @return FlexCollectionInterface Returns a Flex Collection with only matching objects. + * @phpstan-return static + * @api + */ + public function search(string $search, $properties = null, array $options = null); + + /** + * Sort the collection. + * + * @param array $orderings Pair of [property => 'ASC'|'DESC', ...]. + * + * @return FlexCollectionInterface Returns a sorted version from the collection. + * @phpstan-return static + */ + public function sort(array $orderings); + + /** + * Filter collection by filter array with keys and values. + * + * @param array $filters + * @return FlexCollectionInterface + * @phpstan-return static + */ + public function filterBy(array $filters); + + /** + * Get timestamps from all the objects in the collection. + * + * This method can be used for example in caching. + * + * @return int[] Returns [key => timestamp, ...] pairs. + */ + public function getTimestamps(): array; + + /** + * Get storage keys from all the objects in the collection. + * + * @see FlexDirectory::getObject() If you want to get Flex Object from the Flex Directory. + * + * @return string[] Returns [key => storage_key, ...] pairs. + */ + public function getStorageKeys(): array; + + /** + * Get Flex keys from all the objects in the collection. + * + * @see Flex::getObjects() If you want to get list of Flex Objects from any Flex Directory. + * + * @return string[] Returns[key => flex_key, ...] pairs. + */ + public function getFlexKeys(): array; + + /** + * Return new collection with a different key. + * + * @param string|null $keyField Switch key field of the collection. + * @return FlexCollectionInterface Returns a new Flex Collection with new key field. + * @phpstan-return static + * @api + */ + public function withKeyField(string $keyField = null); + + /** + * Get Flex Index from the Flex Collection. + * + * @return FlexIndexInterface Returns a Flex Index from the current collection. + * @phpstan-return FlexIndexInterface + */ + public function getIndex(); + + /** + * Load all the objects into memory, + * + * @return FlexCollectionInterface + * @phpstan-return static + */ + public function getCollection(); + + /** + * Get metadata associated to the object + * + * @param string $key Key. + * @return array + */ + public function getMetaData(string $key): array; +} diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexCommonInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexCommonInterface.php new file mode 100644 index 0000000..ed045e2 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexCommonInterface.php @@ -0,0 +1,79 @@ +getBlueprint() or $object->getForm()->getBlueprint() instead. + * + * @param string $type + * @param string $context + * @return Blueprint + */ + public function getBlueprint(string $type = '', string $context = ''); + + /** + * @param string $view + * @return string + */ + public function getBlueprintFile(string $view = ''): string; + + /** + * Get collection. In the site this will be filtered by the default filters (published etc). + * + * Use $directory->getIndex() if you want unfiltered collection. + * + * @param array|null $keys Array of keys. + * @param string|null $keyField Field to be used as the key. + * @return FlexCollectionInterface + */ + public function getCollection(array $keys = null, string $keyField = null): FlexCollectionInterface; + + /** + * Get the full collection of all stored objects. + * + * Use $directory->getCollection() if you want a filtered collection. + * + * @param array|null $keys Array of keys. + * @param string|null $keyField Field to be used as the key. + * @return FlexIndexInterface + */ + public function getIndex(array $keys = null, string $keyField = null): FlexIndexInterface; + + /** + * Returns an object if it exists. If no arguments are passed (or both of them are null), method creates a new empty object. + * + * Note: It is not safe to use the object without checking if the user can access it. + * + * @param string|null $key + * @param string|null $keyField Field to be used as the key. + * @return FlexObjectInterface|null + */ + public function getObject($key = null, string $keyField = null): ?FlexObjectInterface; + + /** + * @param string|null $namespace + * @return CacheInterface + */ + public function getCache(string $namespace = null); + + /** + * @return $this + */ + public function clearCache(); + + /** + * @param string|null $key + * @return string|null + */ + public function getStorageFolder(string $key = null): ?string; + + /** + * @param string|null $key + * @return string|null + */ + public function getMediaFolder(string $key = null): ?string; + + /** + * @return FlexStorageInterface + */ + public function getStorage(): FlexStorageInterface; + + /** + * @param array $data + * @param string $key + * @param bool $validate + * @return FlexObjectInterface + */ + public function createObject(array $data, string $key = '', bool $validate = false): FlexObjectInterface; + + /** + * @param array $entries + * @param string|null $keyField + * @return FlexCollectionInterface + */ + public function createCollection(array $entries, string $keyField = null): FlexCollectionInterface; + + /** + * @param array $entries + * @param string|null $keyField + * @return FlexIndexInterface + */ + public function createIndex(array $entries, string $keyField = null): FlexIndexInterface; + + /** + * @return string + */ + public function getObjectClass(): string; + + /** + * @return string + */ + public function getCollectionClass(): string; + + /** + * @return string + */ + public function getIndexClass(): string; + + /** + * @param array $entries + * @param string|null $keyField + * @return FlexCollectionInterface + */ + public function loadCollection(array $entries, string $keyField = null): FlexCollectionInterface; + + /** + * @param array $entries + * @return FlexObjectInterface[] + * @internal + */ + public function loadObjects(array $entries): array; + + /** + * @return void + */ + public function reloadIndex(): void; + + /** + * @param string $scope + * @param string $action + * @return string + */ + public function getAuthorizeRule(string $scope, string $action): string; +} diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php new file mode 100644 index 0000000..32dab19 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php @@ -0,0 +1,46 @@ + + */ +interface FlexIndexInterface extends FlexCollectionInterface +{ + /** + * Helper method to create Flex Index. + * + * @used-by FlexDirectory::getIndex() Official method to get Index from a Flex Directory. + * + * @param FlexDirectory $directory Flex directory. + * @return static Returns a new Flex Index. + */ + public static function createFromStorage(FlexDirectory $directory); + + /** + * Method to load index from the object storage, usually filesystem. + * + * @used-by FlexDirectory::getIndex() Official method to get Index from a Flex Directory. + * + * @param FlexStorageInterface $storage Flex Storage associated to the directory. + * @return array Returns a list of existing objects [storage_key => [storage_key => xxx, storage_timestamp => 123456, ...]] + */ + public static function loadEntriesFromStorage(FlexStorageInterface $storage): array; + + /** + * Return new collection with a different key. + * + * @param string|null $keyField Switch key field of the collection. + * @return static Returns a new Flex Collection with new key field. + * @phpstan-return static + * @api + */ + public function withKeyField(string $keyField = null); + + /** + * @param string|null $indexKey + * @return array + */ + public function getIndexMap(string $indexKey = null); +} diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexInterface.php new file mode 100644 index 0000000..3c5a103 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexInterface.php @@ -0,0 +1,98 @@ + + */ + public function getDirectories(array $types = null, bool $keepMissing = false): array; + + /** + * @param string $type + * @return FlexDirectory|null + */ + public function getDirectory(string $type): ?FlexDirectory; + + /** + * @param string $type + * @param array|null $keys + * @param string|null $keyField + * @return FlexCollectionInterface|null + */ + public function getCollection(string $type, array $keys = null, string $keyField = null): ?FlexCollectionInterface; + + /** + * @param array $keys + * @param array $options In addition to the options in getObjects(), following options can be passed: + * collection_class: Class to be used to create the collection. Defaults to ObjectCollection. + * @return FlexCollectionInterface + * @throws RuntimeException + */ + public function getMixedCollection(array $keys, array $options = []): FlexCollectionInterface; + + /** + * @param array $keys + * @param array $options Following optional options can be passed: + * types: List of allowed types. + * type: Allowed type if types isn't defined, otherwise acts as default_type. + * default_type: Set default type for objects given without type (only used if key_field isn't set). + * keep_missing: Set to true if you want to return missing objects as null. + * key_field: Key field which is used to match the objects. + * @return array + */ + public function getObjects(array $keys, array $options = []): array; + + /** + * @param string $key + * @param string|null $type + * @param string|null $keyField + * @return FlexObjectInterface|null + */ + public function getObject(string $key, string $type = null, string $keyField = null): ?FlexObjectInterface; + + /** + * @return int + */ + public function count(): int; +} diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexObjectFormInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexObjectFormInterface.php new file mode 100644 index 0000000..044ee61 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexObjectFormInterface.php @@ -0,0 +1,27 @@ + [storage_key => key, storage_timestamp => timestamp], ...]`. + */ + public function getExistingKeys(): array; + + /** + * Check if the key exists in the storage. + * + * @param string $key Storage key of an object. + * @return bool Returns `true` if the key exists in the storage, `false` otherwise. + */ + public function hasKey(string $key): bool; + + /** + * Check if the key exists in the storage. + * + * @param string[] $keys Storage key of an object. + * @return bool[] Returns keys with `true` if the key exists in the storage, `false` otherwise. + */ + public function hasKeys(array $keys): array; + + /** + * Create new rows into the storage. + * + * New keys will be assigned when the objects are created. + * + * @param array $rows List of rows as `[row, ...]`. + * @return array Returns created rows as `[key => row, ...] pairs. + */ + public function createRows(array $rows): array; + + /** + * Read rows from the storage. + * + * If you pass object or array as value, that value will be used to save I/O. + * + * @param array $rows Array of `[key => row, ...]` pairs. + * @param array|null $fetched Optional reference to store only fetched items. + * @return array Returns rows. Note that non-existing rows will have `null` as their value. + */ + public function readRows(array $rows, array &$fetched = null): array; + + /** + * Update existing rows in the storage. + * + * @param array $rows Array of `[key => row, ...]` pairs. + * @return array Returns updated rows. Note that non-existing rows will not be saved and have `null` as their value. + */ + public function updateRows(array $rows): array; + + /** + * Delete rows from the storage. + * + * @param array $rows Array of `[key => row, ...]` pairs. + * @return array Returns deleted rows. Note that non-existing rows have `null` as their value. + */ + public function deleteRows(array $rows): array; + + /** + * Replace rows regardless if they exist or not. + * + * All rows should have a specified key for replace to work properly. + * + * @param array $rows Array of `[key => row, ...]` pairs. + * @return array Returns both created and updated rows. + */ + public function replaceRows(array $rows): array; + + /** + * @param string $src + * @param string $dst + * @return bool + */ + public function copyRow(string $src, string $dst): bool; + + /** + * @param string $src + * @param string $dst + * @return bool + */ + public function renameRow(string $src, string $dst): bool; + + /** + * Get filesystem path for the collection or object storage. + * + * @param string|null $key Optional storage key. + * @return string|null Path in the filesystem. Can be URI or null if storage is not filesystem based. + */ + public function getStoragePath(string $key = null): ?string; + + /** + * Get filesystem path for the collection or object media. + * + * @param string|null $key Optional storage key. + * @return string|null Path in the filesystem. Can be URI or null if media isn't supported. + */ + public function getMediaPath(string $key = null): ?string; +} diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexTranslateInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexTranslateInterface.php new file mode 100644 index 0000000..28e4888 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexTranslateInterface.php @@ -0,0 +1,51 @@ + + */ +class FlexPageCollection extends FlexCollection +{ + /** + * @return array + */ + public static function getCachedMethods(): array + { + return [ + // Collection filtering + 'withPublished' => true, + 'withVisible' => true, + 'withRoutable' => true, + + 'isFirst' => true, + 'isLast' => true, + + // Find objects + 'prevSibling' => false, + 'nextSibling' => false, + 'adjacentSibling' => false, + 'currentPosition' => true, + + 'getNextOrder' => false, + ] + parent::getCachedMethods(); + } + + /** + * @param bool $bool + * @return static + * @phpstan-return static + */ + public function withPublished(bool $bool = true) + { + $list = array_keys(array_filter($this->call('isPublished', [$bool]))); + + return $this->select($list); + } + + /** + * @param bool $bool + * @return static + * @phpstan-return static + */ + public function withVisible(bool $bool = true) + { + $list = array_keys(array_filter($this->call('isVisible', [$bool]))); + + return $this->select($list); + } + + /** + * @param bool $bool + * @return static + * @phpstan-return static + */ + public function withRoutable(bool $bool = true) + { + $list = array_keys(array_filter($this->call('isRoutable', [$bool]))); + + return $this->select($list); + } + + /** + * Check to see if this item is the first in the collection. + * + * @param string $path + * @return bool True if item is first. + */ + public function isFirst($path): bool + { + $keys = $this->getKeys(); + $first = reset($keys); + + return $path === $first; + } + + /** + * Check to see if this item is the last in the collection. + * + * @param string $path + * @return bool True if item is last. + */ + public function isLast($path): bool + { + $keys = $this->getKeys(); + $last = end($keys); + + return $path === $last; + } + + /** + * Gets the previous sibling based on current position. + * + * @param string $path + * @return PageInterface|false The previous item. + * @phpstan-return T|false + */ + public function prevSibling($path) + { + return $this->adjacentSibling($path, -1); + } + + /** + * Gets the next sibling based on current position. + * + * @param string $path + * @return PageInterface|false The next item. + * @phpstan-return T|false + */ + public function nextSibling($path) + { + return $this->adjacentSibling($path, 1); + } + + /** + * Returns the adjacent sibling based on a direction. + * + * @param string $path + * @param int $direction either -1 or +1 + * @return PageInterface|false The sibling item. + * @phpstan-return T|false + */ + public function adjacentSibling($path, $direction = 1) + { + $keys = $this->getKeys(); + $pos = array_search($path, $keys, true); + + if ($pos !== false) { + $pos += $direction; + if (isset($keys[$pos])) { + return $this[$keys[$pos]]; + } + } + + return false; + } + + /** + * Returns the item in the current position. + * + * @param string $path the path the item + * @return int|null The index of the current page, null if not found. + */ + public function currentPosition($path): ?int + { + $pos = array_search($path, $this->getKeys(), true); + + return $pos !== false ? $pos : null; + } + + /** + * @return string + */ + public function getNextOrder() + { + $directory = $this->getFlexDirectory(); + + $collection = $directory->getIndex(); + $keys = $collection->getStorageKeys(); + + // Assign next free order. + /** @var FlexPageObject|null $last */ + $last = null; + $order = 0; + foreach ($keys as $folder => $key) { + preg_match(FlexPageIndex::ORDER_PREFIX_REGEX, $folder, $test); + $test = $test[0] ?? null; + if ($test && $test > $order) { + $order = $test; + $last = $key; + } + } + + $last = $collection[$last]; + + return sprintf('%d.', $last ? $last->value('order') + 1 : 1); + } +} diff --git a/system/src/Grav/Framework/Flex/Pages/FlexPageIndex.php b/system/src/Grav/Framework/Flex/Pages/FlexPageIndex.php new file mode 100644 index 0000000..904d1f6 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Pages/FlexPageIndex.php @@ -0,0 +1,48 @@ + + */ +class FlexPageIndex extends FlexIndex +{ + public const ORDER_PREFIX_REGEX = '/^\d+\./u'; + + /** + * @param string $route + * @return string + * @internal + */ + public static function normalizeRoute(string $route) + { + static $case_insensitive; + + if (null === $case_insensitive) { + $case_insensitive = Grav::instance()['config']->get('system.force_lowercase_urls', false); + } + + return $case_insensitive ? mb_strtolower($route) : $route; + } +} diff --git a/system/src/Grav/Framework/Flex/Pages/FlexPageObject.php b/system/src/Grav/Framework/Flex/Pages/FlexPageObject.php new file mode 100644 index 0000000..ebc26b4 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Pages/FlexPageObject.php @@ -0,0 +1,495 @@ +header)) { + $this->header = clone($this->header); + } + } + + /** + * @return array + */ + public static function getCachedMethods(): array + { + return [ + // Page Content Interface + 'header' => false, + 'summary' => true, + 'content' => true, + 'value' => false, + 'media' => false, + 'title' => true, + 'menu' => true, + 'visible' => true, + 'published' => true, + 'publishDate' => true, + 'unpublishDate' => true, + 'process' => true, + 'slug' => true, + 'order' => true, + 'id' => true, + 'modified' => true, + 'lastModified' => true, + 'folder' => true, + 'date' => true, + 'dateformat' => true, + 'taxonomy' => true, + 'shouldProcess' => true, + 'isPage' => true, + 'isDir' => true, + 'folderExists' => true, + + // Page + 'isPublished' => true, + 'isOrdered' => true, + 'isVisible' => true, + 'isRoutable' => true, + 'getCreated_Timestamp' => true, + 'getPublish_Timestamp' => true, + 'getUnpublish_Timestamp' => true, + 'getUpdated_Timestamp' => true, + ] + parent::getCachedMethods(); + } + + /** + * @param bool $test + * @return bool + */ + public function isPublished(bool $test = true): bool + { + $time = time(); + $start = $this->getPublish_Timestamp(); + $stop = $this->getUnpublish_Timestamp(); + + return $this->published() && $start <= $time && (!$stop || $time <= $stop) === $test; + } + + /** + * @param bool $test + * @return bool + */ + public function isOrdered(bool $test = true): bool + { + return ($this->order() !== false) === $test; + } + + /** + * @param bool $test + * @return bool + */ + public function isVisible(bool $test = true): bool + { + return $this->visible() === $test; + } + + /** + * @param bool $test + * @return bool + */ + public function isRoutable(bool $test = true): bool + { + return $this->routable() === $test; + } + + /** + * @return int + */ + public function getCreated_Timestamp(): int + { + return $this->getFieldTimestamp('created_date') ?? 0; + } + + /** + * @return int + */ + public function getPublish_Timestamp(): int + { + return $this->getFieldTimestamp('publish_date') ?? $this->getCreated_Timestamp(); + } + + /** + * @return int|null + */ + public function getUnpublish_Timestamp(): ?int + { + return $this->getFieldTimestamp('unpublish_date'); + } + + /** + * @return int + */ + public function getUpdated_Timestamp(): int + { + return $this->getFieldTimestamp('updated_date') ?? $this->getPublish_Timestamp(); + } + + /** + * @inheritdoc + */ + public function getFormValue(string $name, $default = null, string $separator = null) + { + $test = new stdClass(); + + $value = $this->pageContentValue($name, $test); + if ($value !== $test) { + return $value; + } + + switch ($name) { + case 'name': + return $this->getProperty('template'); + case 'route': + return $this->hasKey() ? '/' . $this->getKey() : null; + case 'header.permissions.groups': + $encoded = json_encode($this->getPermissions()); + if ($encoded === false) { + throw new RuntimeException('json_encode(): failed to encode group permissions'); + } + + return json_decode($encoded, true); + } + + return parent::getFormValue($name, $default, $separator); + } + + /** + * Get master storage key. + * + * @return string + * @see FlexObjectInterface::getStorageKey() + */ + public function getMasterKey(): string + { + $key = (string)($this->storage_key ?? $this->getMetaData()['storage_key'] ?? null); + if (($pos = strpos($key, '|')) !== false) { + $key = substr($key, 0, $pos); + } + + return $key; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getCacheKey() + */ + public function getCacheKey(): string + { + return $this->hasKey() ? $this->getTypePrefix() . $this->getFlexType() . '.' . $this->getKey() . '.' . $this->getLanguage() : ''; + } + + /** + * @param string|null $key + * @return FlexObjectInterface + */ + public function createCopy(string $key = null) + { + $this->copy(); + + return parent::createCopy($key); + } + + /** + * @param array|bool $reorder + * @return FlexObject|FlexObjectInterface + */ + public function save($reorder = true) + { + return parent::save(); + } + + /** + * Gets the Page Unmodified (original) version of the page. + * + * Assumes that object has been cloned before modifying it. + * + * @return FlexPageObject|null The original version of the page. + */ + public function getOriginal() + { + return $this->_originalObject; + } + + /** + * Store the Page Unmodified (original) version of the page. + * + * Can be called multiple times, only the first call matters. + * + * @return void + */ + public function storeOriginal(): void + { + if (null === $this->_originalObject) { + $this->_originalObject = clone $this; + } + } + + /** + * Get display order for the associated media. + * + * @return array + */ + public function getMediaOrder(): array + { + $order = $this->getNestedProperty('header.media_order'); + + if (is_array($order)) { + return $order; + } + + if (!$order) { + return []; + } + + return array_map('trim', explode(',', $order)); + } + + // Overrides for header properties. + + /** + * Common logic to load header properties. + * + * @param string $property + * @param mixed $var + * @param callable $filter + * @return mixed|null + */ + protected function loadHeaderProperty(string $property, $var, callable $filter) + { + // We have to use parent methods in order to avoid loops. + $value = null === $var ? parent::getProperty($property) : null; + if (null === $value) { + $value = $filter($var ?? $this->getProperty('header')->get($property)); + + parent::setProperty($property, $value); + if ($this->doHasProperty($property)) { + $value = parent::getProperty($property); + } + } + + return $value; + } + + /** + * Common logic to load header properties. + * + * @param string $property + * @param mixed $var + * @param callable $filter + * @return mixed|null + */ + protected function loadProperty(string $property, $var, callable $filter) + { + // We have to use parent methods in order to avoid loops. + $value = null === $var ? parent::getProperty($property) : null; + if (null === $value) { + $value = $filter($var); + + parent::setProperty($property, $value); + if ($this->doHasProperty($property)) { + $value = parent::getProperty($property); + } + } + + return $value; + } + + /** + * @param string $property + * @param mixed $default + * @return mixed + */ + public function getProperty($property, $default = null) + { + $method = static::$headerProperties[$property] ?? static::$calculatedProperties[$property] ?? null; + if ($method && method_exists($this, $method)) { + return $this->{$method}(); + } + + return parent::getProperty($property, $default); + } + + /** + * @param string $property + * @param mixed $value + * @return $this + */ + public function setProperty($property, $value) + { + $method = static::$headerProperties[$property] ?? static::$calculatedProperties[$property] ?? null; + if ($method && method_exists($this, $method)) { + $this->{$method}($value); + + return $this; + } + + parent::setProperty($property, $value); + + return $this; + } + + /** + * @param string $property + * @param mixed $value + * @param string|null $separator + * @return $this + */ + public function setNestedProperty($property, $value, $separator = null) + { + $separator = $separator ?: '.'; + if (strpos($property, 'header' . $separator) === 0) { + $this->getProperty('header')->set(str_replace('header' . $separator, '', $property), $value, $separator); + + return $this; + } + + parent::setNestedProperty($property, $value, $separator); + + return $this; + } + + /** + * @param string $property + * @param string|null $separator + * @return $this + */ + public function unsetNestedProperty($property, $separator = null) + { + $separator = $separator ?: '.'; + if (strpos($property, 'header' . $separator) === 0) { + $this->getProperty('header')->undef(str_replace('header' . $separator, '', $property), $separator); + + return $this; + } + + parent::unsetNestedProperty($property, $separator); + + return $this; + } + + /** + * @param array $elements + * @param bool $extended + * @return void + */ + protected function filterElements(array &$elements, bool $extended = false): void + { + // Markdown storage conversion to page structure. + if (array_key_exists('content', $elements)) { + $elements['markdown'] = $elements['content']; + unset($elements['content']); + } + + if (!$extended) { + $folder = !empty($elements['folder']) ? trim($elements['folder']) : ''; + + if ($folder) { + $order = !empty($elements['order']) ? (int)$elements['order'] : null; + // TODO: broken + $elements['storage_key'] = $order ? sprintf('%02d.%s', $order, $folder) : $folder; + } + } + + parent::filterElements($elements); + } + + /** + * @param string $field + * @return int|null + */ + protected function getFieldTimestamp(string $field): ?int + { + $date = $this->getFieldDateTime($field); + + return $date ? $date->getTimestamp() : null; + } + + /** + * @param string $field + * @return DateTime|null + */ + protected function getFieldDateTime(string $field): ?DateTime + { + try { + $value = $this->getProperty($field); + if (is_numeric($value)) { + $value = '@' . $value; + } + $date = $value ? new DateTime($value) : null; + } catch (Exception $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + $date = null; + } + + return $date; + } + + /** + * @return UserCollectionInterface|null + * @internal + */ + protected function loadAccounts() + { + return Grav::instance()['accounts'] ?? null; + } +} diff --git a/system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php b/system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php new file mode 100644 index 0000000..5d3e968 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php @@ -0,0 +1,249 @@ + */ + private $_authors; + /** @var array|null */ + private $_permissionsCache; + + /** + * Returns true if object has the named author. + * + * @param string $username + * @return bool + */ + public function hasAuthor(string $username): bool + { + $authors = (array)$this->getNestedProperty('header.permissions.authors'); + if (empty($authors)) { + return false; + } + + foreach ($authors as $author) { + if ($username === $author) { + return true; + } + } + + return false; + } + + /** + * Get list of all author objects. + * + * @return array + */ + public function getAuthors(): array + { + if (null === $this->_authors) { + $this->_authors = $this->loadAuthors($this->getNestedProperty('header.permissions.authors', [])); + } + + return $this->_authors; + } + + /** + * @param bool $inherit + * @return array + */ + public function getPermissions(bool $inherit = false) + { + if (null === $this->_permissionsCache) { + $permissions = []; + if ($inherit && $this->getNestedProperty('header.permissions.inherit', true)) { + $parent = $this->parent(); + if ($parent && method_exists($parent, 'getPermissions')) { + $permissions = $parent->getPermissions($inherit); + } + } + + $this->_permissionsCache = $this->loadPermissions($permissions); + } + + return $this->_permissionsCache; + } + + /** + * @param iterable $authors + * @return array + */ + protected function loadAuthors(iterable $authors): array + { + $accounts = $this->loadAccounts(); + if (null === $accounts || empty($authors)) { + return []; + } + + $list = []; + foreach ($authors as $username) { + if (!is_string($username)) { + throw new InvalidArgumentException('Iterable should return username (string).', 500); + } + $list[] = $accounts->load($username); + } + + return $list; + } + + /** + * @param string $action + * @param string|null $scope + * @param UserInterface|null $user + * @param bool $isAuthor + * @return bool|null + */ + public function isParentAuthorized(string $action, string $scope = null, UserInterface $user = null, bool $isAuthor = false): ?bool + { + $scope = $scope ?? $this->getAuthorizeScope(); + + $isMe = null === $user; + if ($isMe) { + $user = $this->getActiveUser(); + } + + if (null === $user) { + return false; + } + + return $this->isAuthorizedByGroup($user, $action, $scope, $isMe, $isAuthor); + } + + /** + * @param UserInterface $user + * @param string $action + * @param string $scope + * @param bool $isMe + * @return bool|null + */ + protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool + { + if ($action === 'delete' && $this->root()) { + // Do not allow deleting root. + return false; + } + + $isAuthor = !$isMe || $user->authorized ? $this->hasAuthor($user->username) : false; + + return $this->isAuthorizedByGroup($user, $action, $scope, $isMe, $isAuthor) ?? parent::isAuthorizedOverride($user, $action, $scope, $isMe); + } + + /** + * Group authorization works as follows: + * + * 1. if any of the groups deny access, return false + * 2. else if any of the groups allow access, return true + * 3. else return null + * + * @param UserInterface $user + * @param string $action + * @param string $scope + * @param bool $isMe + * @param bool $isAuthor + * @return bool|null + */ + protected function isAuthorizedByGroup(UserInterface $user, string $action, string $scope, bool $isMe, bool $isAuthor): ?bool + { + $authorized = null; + + // In admin we want to check against group permissions. + $pageGroups = $this->getPermissions(); + $userGroups = (array)$user->groups; + + /** @var Access $access */ + foreach ($pageGroups as $group => $access) { + if ($group === 'defaults') { + // Special defaults permissions group does not apply to guest. + if ($isMe && !$user->authorized) { + continue; + } + } elseif ($group === 'authors') { + if (!$isAuthor) { + continue; + } + } elseif (!in_array($group, $userGroups, true)) { + continue; + } + + $auth = $access->authorize($action); + if (is_bool($auth)) { + if ($auth === false) { + return false; + } + + $authorized = true; + } + } + + if (null === $authorized && $this->getNestedProperty('header.permissions.inherit', true)) { + // Authorize against parent page. + $parent = $this->parent(); + if ($parent && method_exists($parent, 'isParentAuthorized')) { + $authorized = $parent->isParentAuthorized($action, $scope, !$isMe ? $user : null, $isAuthor); + } + } + + return $authorized; + } + + /** + * @param array $parent + * @return array + */ + protected function loadPermissions(array $parent = []): array + { + static $rules = [ + 'c' => 'create', + 'r' => 'read', + 'u' => 'update', + 'd' => 'delete', + 'p' => 'publish', + 'l' => 'list' + ]; + + $permissions = $this->getNestedProperty('header.permissions.groups'); + $name = $this->root() ? '' : '/' . $this->getKey(); + + $list = []; + if (is_array($permissions)) { + foreach ($permissions as $group => $access) { + $list[$group] = new Access($access, $rules, $name); + } + } + foreach ($parent as $group => $access) { + if (isset($list[$group])) { + $object = $list[$group]; + } else { + $object = new Access([], $rules, $name); + $list[$group] = $object; + } + + $object->inherit($access); + } + + return $list; + } +} diff --git a/system/src/Grav/Framework/Flex/Pages/Traits/PageContentTrait.php b/system/src/Grav/Framework/Flex/Pages/Traits/PageContentTrait.php new file mode 100644 index 0000000..6aff732 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Pages/Traits/PageContentTrait.php @@ -0,0 +1,840 @@ + 'slug', + 'routes' => false, + 'title' => 'title', + 'language' => 'language', + 'template' => 'template', + 'menu' => 'menu', + 'routable' => 'routable', + 'visible' => 'visible', + 'redirect' => 'redirect', + 'external_url' => false, + 'order_dir' => 'orderDir', + 'order_by' => 'orderBy', + 'order_manual' => 'orderManual', + 'dateformat' => 'dateformat', + 'date' => 'date', + 'markdown_extra' => false, + 'taxonomy' => 'taxonomy', + 'max_count' => 'maxCount', + 'process' => 'process', + 'published' => 'published', + 'publish_date' => 'publishDate', + 'unpublish_date' => 'unpublishDate', + 'expires' => 'expires', + 'cache_control' => 'cacheControl', + 'etag' => 'eTag', + 'last_modified' => 'lastModified', + 'ssl' => 'ssl', + 'template_format' => 'templateFormat', + 'debugger' => false, + ]; + + /** @var array */ + protected static $calculatedProperties = [ + 'name' => 'name', + 'parent' => 'parent', + 'parent_key' => 'parentStorageKey', + 'folder' => 'folder', + 'order' => 'order', + 'template' => 'template', + ]; + + /** @var object */ + protected $header; + + /** @var string */ + protected $_summary; + + /** @var string */ + protected $_content; + + /** + * Method to normalize the route. + * + * @param string $route + * @return string + * @internal + */ + public static function normalizeRoute($route): string + { + $case_insensitive = Grav::instance()['config']->get('system.force_lowercase_urls'); + + return $case_insensitive ? mb_strtolower($route) : $route; + } + + /** + * @inheritdoc + */ + public function header($var = null) + { + if (null !== $var) { + $this->setProperty('header', $var); + } + + return $this->getProperty('header'); + } + + /** + * @inheritdoc + */ + public function summary($size = null, $textOnly = false): string + { + return $this->processSummary($size, $textOnly); + } + + /** + * @inheritdoc + */ + public function setSummary($summary): void + { + $this->_summary = $summary; + } + + /** + * @inheritdoc + * @throws Exception + */ + public function content($var = null): string + { + if (null !== $var) { + $this->_content = $var; + } + + return $this->_content ?? $this->processContent($this->getRawContent()); + } + + /** + * @inheritdoc + */ + public function getRawContent(): string + { + return $this->_content ?? $this->getArrayProperty('markdown') ?? ''; + } + + /** + * @inheritdoc + */ + public function setRawContent($content): void + { + $this->_content = $content ?? ''; + } + + /** + * @inheritdoc + */ + public function rawMarkdown($var = null): string + { + if ($var !== null) { + $this->setProperty('markdown', $var); + } + + return $this->getProperty('markdown') ?? ''; + } + + /** + * @inheritdoc + * + * Implement by calling: + * + * $test = new \stdClass(); + * $value = $this->pageContentValue($name, $test); + * if ($value !== $test) { + * return $value; + * } + * return parent::value($name, $default); + */ + abstract public function value($name, $default = null, $separator = null); + + /** + * @inheritdoc + */ + public function media($var = null): Media + { + if ($var instanceof Media) { + $this->setProperty('media', $var); + } + + return $this->getProperty('media'); + } + + /** + * @inheritdoc + */ + public function title($var = null): string + { + return $this->loadHeaderProperty( + 'title', + $var, + function ($value) { + return trim($value ?? ($this->root() ? '' : ucfirst($this->slug()))); + } + ); + } + + /** + * @inheritdoc + */ + public function menu($var = null): string + { + return $this->loadHeaderProperty( + 'menu', + $var, + function ($value) { + return trim($value ?: $this->title()); + } + ); + } + + /** + * @inheritdoc + */ + public function visible($var = null): bool + { + $value = $this->loadHeaderProperty( + 'visible', + $var, + function ($value) { + return ($value ?? $this->order() !== false) && !$this->isModule(); + } + ); + + return $value && $this->published(); + } + + /** + * @inheritdoc + */ + public function published($var = null): bool + { + return $this->loadHeaderProperty( + 'published', + $var, + static function ($value) { + return (bool)($value ?? true); + } + ); + } + + /** + * @inheritdoc + */ + public function publishDate($var = null): ?int + { + return $this->loadHeaderProperty( + 'publish_date', + $var, + function ($value) { + return $value ? Utils::date2timestamp($value, $this->getProperty('dateformat')) : null; + } + ); + } + + /** + * @inheritdoc + */ + public function unpublishDate($var = null): ?int + { + return $this->loadHeaderProperty( + 'unpublish_date', + $var, + function ($value) { + return $value ? Utils::date2timestamp($value, $this->getProperty('dateformat')) : null; + } + ); + } + + /** + * @inheritdoc + */ + public function process($var = null): array + { + return $this->loadHeaderProperty( + 'process', + $var, + function ($value) { + $value = array_replace(Grav::instance()['config']->get('system.pages.process', []), is_array($value) ? $value : []) ?? []; + foreach ($value as $process => $status) { + $value[$process] = (bool)$status; + } + + return $value; + } + ); + } + + /** + * @inheritdoc + */ + public function slug($var = null) + { + return $this->loadHeaderProperty( + 'slug', + $var, + function ($value) { + if (is_string($value)) { + return $value; + } + + $folder = $this->folder(); + if (null === $folder) { + return null; + } + + $folder = preg_replace(static::PAGE_ORDER_PREFIX_REGEX, '', $folder); + if (null === $folder) { + return null; + } + + return static::normalizeRoute($folder); + } + ); + } + + /** + * @inheritdoc + */ + public function order($var = null) + { + $property = $this->loadProperty( + 'order', + $var, + function ($value) { + if (null === $value) { + $folder = $this->folder(); + if (null !== $folder) { + preg_match(static::PAGE_ORDER_REGEX, $folder, $order); + } + + $value = $order[1] ?? false; + } + + if ($value === '') { + $value = false; + } + if ($value !== false) { + $value = (int)$value; + } + + return $value; + } + ); + + return $property !== false ? sprintf('%02d.', $property) : false; + } + + /** + * @inheritdoc + */ + public function id($var = null): string + { + $property = 'id'; + $value = null === $var ? $this->getProperty($property) : null; + if (null === $value) { + $value = $this->language() . ($var ?? ($this->modified() . md5('flex-' . $this->getFlexType() . '-' . $this->getKey()))); + + $this->setProperty($property, $value); + if ($this->doHasProperty($property)) { + $value = $this->getProperty($property); + } + } + + return $value; + } + + /** + * @inheritdoc + */ + public function modified($var = null): int + { + $property = 'modified'; + $value = null === $var ? $this->getProperty($property) : null; + if (null === $value) { + $value = (int)($var ?: $this->getTimestamp()); + + $this->setProperty($property, $value); + if ($this->doHasProperty($property)) { + $value = $this->getProperty($property); + } + } + + return $value; + } + + /** + * @inheritdoc + */ + public function lastModified($var = null): bool + { + return $this->loadHeaderProperty( + 'last_modified', + $var, + static function ($value) { + return (bool)($value ?? Grav::instance()['config']->get('system.pages.last_modified')); + } + ); + } + + /** + * @inheritdoc + */ + public function date($var = null): int + { + return $this->loadHeaderProperty( + 'date', + $var, + function ($value) { + $value = $value ? Utils::date2timestamp($value, $this->getProperty('dateformat')) : false; + + return $value ?: $this->modified(); + } + ); + } + + /** + * @inheritdoc + */ + public function dateformat($var = null): ?string + { + return $this->loadHeaderProperty( + 'dateformat', + $var, + static function ($value) { + return $value; + } + ); + } + + /** + * @inheritdoc + */ + public function taxonomy($var = null): array + { + return $this->loadHeaderProperty( + 'taxonomy', + $var, + static function ($value) { + if (is_array($value)) { + // make sure first level are arrays + array_walk($value, static function (&$val) { + $val = (array) $val; + }); + // make sure all values are strings + array_walk_recursive($value, static function (&$val) { + $val = (string) $val; + }); + } + + return $value ?? []; + } + ); + } + + /** + * @inheritdoc + */ + public function shouldProcess($process): bool + { + $test = $this->process(); + + return !empty($test[$process]); + } + + /** + * @inheritdoc + */ + public function isPage(): bool + { + return !in_array($this->template(), ['', 'folder'], true); + } + + /** + * @inheritdoc + */ + public function isDir(): bool + { + return !$this->isPage(); + } + + /** + * @return bool + */ + public function isModule(): bool + { + return $this->modularTwig(); + } + + /** + * @param Header|stdClass|array|null $value + * @return Header + */ + protected function offsetLoad_header($value) + { + if ($value instanceof Header) { + return $value; + } + + if (null === $value) { + $value = []; + } elseif ($value instanceof stdClass) { + $value = (array)$value; + } + + return new Header($value); + } + + /** + * @param Header|stdClass|array|null $value + * @return Header + */ + protected function offsetPrepare_header($value) + { + return $this->offsetLoad_header($value); + } + + /** + * @param Header|null $value + * @return array + */ + protected function offsetSerialize_header(?Header $value) + { + return $value ? $value->toArray() : []; + } + + /** + * @param string $name + * @param mixed|null $default + * @return mixed + */ + protected function pageContentValue($name, $default = null) + { + switch ($name) { + case 'frontmatter': + $frontmatter = $this->getArrayProperty('frontmatter'); + if ($frontmatter === null) { + $header = $this->prepareStorage()['header'] ?? null; + if ($header) { + $formatter = new YamlFormatter(); + $frontmatter = $formatter->encode($header); + } else { + $frontmatter = ''; + } + } + return $frontmatter; + case 'content': + return $this->getProperty('markdown'); + case 'order': + return (string)$this->order(); + case 'menu': + return $this->menu(); + case 'ordering': + return $this->order() !== false ? '1' : '0'; + case 'folder': + $folder = $this->folder(); + + return null !== $folder ? preg_replace(static::PAGE_ORDER_PREFIX_REGEX, '', $folder) : ''; + case 'slug': + return $this->slug(); + case 'published': + return $this->published(); + case 'visible': + return $this->visible(); + case 'media': + return $this->media()->all(); + case 'media.file': + return $this->media()->files(); + case 'media.video': + return $this->media()->videos(); + case 'media.image': + return $this->media()->images(); + case 'media.audio': + return $this->media()->audios(); + } + + return $default; + } + + /** + * @param int|null $size + * @param bool $textOnly + * @return string + */ + protected function processSummary($size = null, $textOnly = false): string + { + $config = (array)Grav::instance()['config']->get('site.summary'); + $config_page = (array)$this->getNestedProperty('header.summary'); + if ($config_page) { + $config = array_merge($config, $config_page); + } + + // Summary is not enabled, return the whole content. + if (empty($config['enabled'])) { + return $this->content(); + } + + $content = $this->_summary ?? $this->content(); + if ($textOnly) { + $content = strip_tags($content); + } + $content_size = mb_strwidth($content, 'utf-8'); + $summary_size = $this->_summary !== null ? $content_size : $this->getProperty('summary_size'); + + // Return calculated summary based on summary divider's position. + $format = $config['format'] ?? ''; + + // Return entire page content on wrong/unknown format. + if ($format !== 'long' && $format !== 'short') { + return $content; + } + + if ($format === 'short' && null !== $summary_size) { + // Slice the string on breakpoint. + if ($content_size > $summary_size) { + return mb_substr($content, 0, $summary_size); + } + + return $content; + } + + // If needed, get summary size from the config. + $size = $size ?? $config['size'] ?? null; + + // Return calculated summary based on defaults. + $size = is_numeric($size) ? (int)$size : -1; + if ($size < 0) { + $size = 300; + } + + // If the size is zero or smaller than the summary limit, return the entire page content. + if ($size === 0 || $content_size <= $size) { + return $content; + } + + // Only return string but not html, wrap whatever html tag you want when using. + if ($textOnly) { + return mb_strimwidth($content, 0, $size, '...', 'UTF-8'); + } + + $summary = Utils::truncateHTML($content, $size); + + return html_entity_decode($summary, ENT_COMPAT | ENT_HTML5, 'UTF-8'); + } + + /** + * Gets and Sets the content based on content portion of the .md file + * + * @param string $content + * @return string + * @throws Exception + */ + protected function processContent($content): string + { + $grav = Grav::instance(); + + /** @var Config $config */ + $config = $grav['config']; + + $process_markdown = $this->shouldProcess('markdown'); + $process_twig = $this->shouldProcess('twig') || $this->isModule(); + $cache_enable = $this->getNestedProperty('header.cache_enable') ?? $config->get('system.cache.enabled', true); + + $twig_first = $this->getNestedProperty('header.twig_first') ?? $config->get('system.pages.twig_first', false); + $never_cache_twig = $this->getNestedProperty('header.never_cache_twig') ?? $config->get('system.pages.never_cache_twig', false); + + $cached = null; + if ($cache_enable) { + $cache = $this->getCache('render'); + $key = md5($this->getCacheKey() . '-content'); + $cached = $cache->get($key); + if ($cached && $cached['checksum'] === $this->getCacheChecksum()) { + $this->_content = $cached['content'] ?? ''; + $this->_content_meta = $cached['content_meta'] ?? null; + + if ($process_twig && $never_cache_twig) { + $this->_content = $this->processTwig($this->_content); + } + } else { + $cached = null; + } + } + + if (!$cached) { + $markdown_options = []; + if ($process_markdown) { + // Build markdown options. + $markdown_options = (array)$config->get('system.pages.markdown'); + $markdown_page_options = (array)$this->getNestedProperty('header.markdown'); + if ($markdown_page_options) { + $markdown_options = array_merge($markdown_options, $markdown_page_options); + } + + // pages.markdown_extra is deprecated, but still check it... + if (!isset($markdown_options['extra'])) { + $extra = $this->getNestedProperty('header.markdown_extra') ?? $config->get('system.pages.markdown_extra'); + if (null !== $extra) { + user_error('Configuration option \'system.pages.markdown_extra\' is deprecated since Grav 1.5, use \'system.pages.markdown.extra\' instead', E_USER_DEPRECATED); + + $markdown_options['extra'] = $extra; + } + } + } + $options = [ + 'markdown' => $markdown_options, + 'images' => $config->get('system.images', []) + ]; + + $this->_content = $content; + $grav->fireEvent('onPageContentRaw', new Event(['page' => $this])); + + if ($twig_first && !$never_cache_twig) { + if ($process_twig) { + $this->_content = $this->processTwig($this->_content); + } + + if ($process_markdown) { + $this->_content = $this->processMarkdown($this->_content, $options); + } + + // Content Processed but not cached yet + $grav->fireEvent('onPageContentProcessed', new Event(['page' => $this])); + } else { + if ($process_markdown) { + $options['keep_twig'] = $process_twig; + $this->_content = $this->processMarkdown($this->_content, $options); + } + + // Content Processed but not cached yet + $grav->fireEvent('onPageContentProcessed', new Event(['page' => $this])); + + if ($cache_enable && $never_cache_twig) { + $this->cachePageContent(); + } + + if ($process_twig) { + $this->_content = $this->processTwig($this->_content); + } + } + + if ($cache_enable && !$never_cache_twig) { + $this->cachePageContent(); + } + } + + // Handle summary divider + $delimiter = $config->get('site.summary.delimiter', '==='); + $divider_pos = mb_strpos($this->_content, "

    {$delimiter}

    "); + if ($divider_pos !== false) { + $this->setProperty('summary_size', $divider_pos); + $this->_content = str_replace("

    {$delimiter}

    ", '', $this->_content); + } + + // Fire event when Page::content() is called + $grav->fireEvent('onPageContent', new Event(['page' => $this])); + + return $this->_content; + } + + /** + * Process the Twig page content. + * + * @param string $content + * @return string + */ + protected function processTwig($content): string + { + /** @var Twig $twig */ + $twig = Grav::instance()['twig']; + + /** @var PageInterface $this */ + return $twig->processPage($this, $content); + } + + /** + * Process the Markdown content. + * + * Uses Parsedown or Parsedown Extra depending on configuration. + * + * @param string $content + * @param array $options + * @return string + * @throws Exception + */ + protected function processMarkdown($content, array $options = []): string + { + /** @var PageInterface $self */ + $self = $this; + + $excerpts = new Excerpts($self, $options); + + // Initialize the preferred variant of markdown parser. + if (isset($options['extra'])) { + $parsedown = new ParsedownExtra($excerpts); + } else { + $parsedown = new Parsedown($excerpts); + } + + $keepTwig = (bool)($options['keep_twig'] ?? false); + if ($keepTwig) { + $token = [ + '/' . Utils::generateRandomString(3), + Utils::generateRandomString(3) . '/' + ]; + // Base64 encode any twig. + $content = preg_replace_callback( + ['/({#.*?#})/mu', '/({{.*?}})/mu', '/({%.*?%})/mu'], + static function ($matches) use ($token) { return $token[0] . base64_encode($matches[1]) . $token[1]; }, + $content + ); + } + + $content = $parsedown->text($content); + + if ($keepTwig) { + // Base64 decode the encoded twig. + $content = preg_replace_callback( + ['`' . $token[0] . '([A-Za-z0-9+/]+={0,2})' . $token[1] . '`mu'], + static function ($matches) { return base64_decode($matches[1]); }, + $content + ); + } + + return $content; + } + + abstract protected function loadHeaderProperty(string $property, $var, callable $filter); +} diff --git a/system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php b/system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php new file mode 100644 index 0000000..6453bcf --- /dev/null +++ b/system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php @@ -0,0 +1,1119 @@ +getFlexDirectory()->getStorage(); + if (method_exists($storage, 'readRaw')) { + return $storage->readRaw($this->getStorageKey()); + } + + $array = $this->prepareStorage(); + $formatter = new MarkdownFormatter(); + + return $formatter->encode($array); + } + + /** + * Gets and Sets the page frontmatter + * + * @param string|null $var + * @return string + */ + public function frontmatter($var = null): string + { + if (null !== $var) { + $formatter = new YamlFormatter(); + $this->setProperty('frontmatter', $var); + $this->setProperty('header', $formatter->decode($var)); + + return $var; + } + + $storage = $this->getFlexDirectory()->getStorage(); + if (method_exists($storage, 'readFrontmatter')) { + return $storage->readFrontmatter($this->getStorageKey()); + } + + $array = $this->prepareStorage(); + $formatter = new YamlFormatter(); + + return $formatter->encode($array['header'] ?? []); + } + + /** + * Modify a header value directly + * + * @param string $key + * @param string|array $value + * @return void + */ + public function modifyHeader($key, $value): void + { + $this->setNestedProperty("header.{$key}", $value); + } + + /** + * @return int + */ + public function httpResponseCode(): int + { + $code = (int)$this->getNestedProperty('header.http_response_code'); + + return $code ?: 200; + } + + /** + * @return array + */ + public function httpHeaders(): array + { + $headers = []; + + $format = $this->templateFormat(); + $cache_control = $this->cacheControl(); + $expires = $this->expires(); + + // Set Content-Type header. + $headers['Content-Type'] = Utils::getMimeByExtension($format, 'text/html'); + + // Calculate Expires Headers if set to > 0. + if ($expires > 0) { + $expires_date = gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT'; + if (!$cache_control) { + $headers['Cache-Control'] = 'max-age=' . $expires; + } + $headers['Expires'] = $expires_date; + } + + // Set Cache-Control header. + if ($cache_control) { + $headers['Cache-Control'] = strtolower($cache_control); + } + + // Set Last-Modified header. + if ($this->lastModified()) { + $last_modified_date = gmdate('D, d M Y H:i:s', $this->modified()) . ' GMT'; + $headers['Last-Modified'] = $last_modified_date; + } + + // Calculate ETag based on the serialized page and modified time. + if ($this->eTag()) { + $headers['ETag'] = '1'; + } + + // Set Vary: Accept-Encoding header. + $grav = Grav::instance(); + if ($grav['config']->get('system.pages.vary_accept_encoding', false)) { + $headers['Vary'] = 'Accept-Encoding'; + } + + return $headers; + } + + /** + * Get the contentMeta array and initialize content first if it's not already + * + * @return array + */ + public function contentMeta(): array + { + // Content meta is generated during the content is being rendered, so make sure we have done it. + $this->content(); + + return $this->_content_meta ?? []; + } + + /** + * Add an entry to the page's contentMeta array + * + * @param string $name + * @param string $value + * @return void + */ + public function addContentMeta($name, $value): void + { + $this->_content_meta[$name] = $value; + } + + /** + * Return the whole contentMeta array as it currently stands + * + * @param string|null $name + * @return string|array|null + */ + public function getContentMeta($name = null) + { + if ($name) { + return $this->_content_meta[$name] ?? null; + } + + return $this->_content_meta ?? []; + } + + /** + * Sets the whole content meta array in one shot + * + * @param array $content_meta + * @return array + */ + public function setContentMeta($content_meta): array + { + return $this->_content_meta = $content_meta; + } + + /** + * Fires the onPageContentProcessed event, and caches the page content using a unique ID for the page + */ + public function cachePageContent(): void + { + $value = [ + 'checksum' => $this->getCacheChecksum(), + 'content' => $this->_content, + 'content_meta' => $this->_content_meta + ]; + + $cache = $this->getCache('render'); + $key = md5($this->getCacheKey() . '-content'); + + $cache->set($key, $value); + } + + /** + * Get file object to the page. + * + * @return MarkdownFile|null + */ + public function file(): ?MarkdownFile + { + // TODO: + throw new RuntimeException(__METHOD__ . '(): Not Implemented'); + } + + /** + * Prepare move page to new location. Moves also everything that's under the current page. + * + * You need to call $this->save() in order to perform the move. + * + * @param PageInterface $parent New parent page. + * @return $this + */ + public function move(PageInterface $parent) + { + if ($this->route() === $parent->route()) { + throw new RuntimeException('Failed: Cannot set page parent to self'); + } + $rawRoute = $this->rawRoute(); + if ($rawRoute && Utils::startsWith($parent->rawRoute(), $rawRoute)) { + throw new RuntimeException('Failed: Cannot set page parent to a child of current page'); + } + + $this->storeOriginal(); + + // TODO: + throw new RuntimeException(__METHOD__ . '(): Not Implemented'); + } + + /** + * Prepare a copy from the page. Copies also everything that's under the current page. + * + * Returns a new Page object for the copy. + * You need to call $this->save() in order to perform the move. + * + * @param PageInterface|null $parent New parent page. + * @return $this + */ + public function copy(PageInterface $parent = null) + { + $this->storeOriginal(); + + $filesystem = Filesystem::getInstance(false); + + $parentStorageKey = ltrim($filesystem->dirname("/{$this->getMasterKey()}"), '/'); + + /** @var FlexPageIndex $index */ + $index = $this->getFlexDirectory()->getIndex(); + + if ($parent) { + if ($parent instanceof FlexPageObject) { + $k = $parent->getMasterKey(); + if ($k !== $parentStorageKey) { + $parentStorageKey = $k; + } + } else { + throw new RuntimeException('Cannot copy page, parent is of unknown type'); + } + } else { + $parent = $parentStorageKey + ? $this->getFlexDirectory()->getObject($parentStorageKey, 'storage_key') + : (method_exists($index, 'getRoot') ? $index->getRoot() : null); + } + + // Find non-existing key. + $parentKey = $parent ? $parent->getKey() : ''; + if ($this instanceof FlexPageObject) { + $key = trim($parentKey . '/' . $this->folder(), '/'); + $key = preg_replace(static::PAGE_ORDER_PREFIX_REGEX, '', $key); + } else { + $key = trim($parentKey . '/' . basename($this->getKey()), '/'); + } + + if ($index->containsKey($key)) { + $key = preg_replace('/\d+$/', '', $key); + $i = 1; + do { + $i++; + $test = "{$key}{$i}"; + } while ($index->containsKey($test)); + $key = $test; + } + $folder = basename($key); + + // Get the folder name. + $order = $this->getProperty('order'); + if ($order) { + $order++; + } + + $parts = []; + if ($parentStorageKey !== '') { + $parts[] = $parentStorageKey; + } + $parts[] = $order ? sprintf('%02d.%s', $order, $folder) : $folder; + + // Finally update the object. + $this->setKey($key); + $this->setStorageKey(implode('/', $parts)); + + $this->markAsCopy(); + + return $this; + } + + /** + * Get the blueprint name for this page. Use the blueprint form field if set + * + * @return string + */ + public function blueprintName(): string + { + $blueprint_name = filter_input(INPUT_POST, 'blueprint', FILTER_SANITIZE_STRING) ?: $this->template(); + + return $blueprint_name; + } + + /** + * Validate page header. + * + * @return void + * @throws Exception + */ + public function validate(): void + { + $blueprint = $this->getBlueprint(); + $blueprint->validate($this->toArray()); + } + + /** + * Filter page header from illegal contents. + * + * @return void + */ + public function filter(): void + { + $blueprints = $this->getBlueprint(); + $values = $blueprints->filter($this->toArray()); + if ($values && isset($values['header'])) { + $this->header($values['header']); + } + } + + /** + * Get unknown header variables. + * + * @return array + */ + public function extra(): array + { + $data = $this->prepareStorage(); + + return $this->getBlueprint()->extra((array)($data['header'] ?? []), 'header.'); + } + + /** + * Convert page to an array. + * + * @return array + */ + public function toArray(): array + { + return [ + 'header' => (array)$this->header(), + 'content' => (string)$this->getFormValue('content') + ]; + } + + /** + * Convert page to YAML encoded string. + * + * @return string + */ + public function toYaml(): string + { + return Yaml::dump($this->toArray(), 20); + } + + /** + * Convert page to JSON encoded string. + * + * @return string + */ + public function toJson(): string + { + $json = json_encode($this->toArray()); + if (!is_string($json)) { + throw new RuntimeException('Internal error'); + } + + return $json; + } + + /** + * Gets and sets the name field. If no name field is set, it will return 'default.md'. + * + * @param string|null $var The name of this page. + * @return string The name of this page. + */ + public function name($var = null): string + { + return $this->loadProperty( + 'name', + $var, + function ($value) { + $value = $value ?? $this->getMetaData()['template'] ?? 'default'; + if (!preg_match('/\.md$/', $value)) { + $language = $this->language(); + if ($language) { + // TODO: better language support + $value .= ".{$language}"; + } + $value .= '.md'; + } + $value = preg_replace('|^modular/|', '', $value); + + $this->unsetProperty('template'); + + return $value; + } + ); + } + + /** + * Returns child page type. + * + * @return string + */ + public function childType(): string + { + return (string)$this->getNestedProperty('header.child_type'); + } + + /** + * Gets and sets the template field. This is used to find the correct Twig template file to render. + * If no field is set, it will return the name without the .md extension + * + * @param string|null $var the template name + * @return string the template name + */ + public function template($var = null): string + { + return $this->loadHeaderProperty( + 'template', + $var, + function ($value) { + return trim($value ?? (($this->isModule() ? 'modular/' : '') . str_replace($this->extension(), '', $this->name()))); + } + ); + } + + /** + * Allows a page to override the output render format, usually the extension provided in the URL. + * (e.g. `html`, `json`, `xml`, etc). + * + * @param string|null $var + * @return string + */ + public function templateFormat($var = null): string + { + return $this->loadHeaderProperty( + 'template_format', + $var, + function ($value) { + return ltrim($value ?? $this->getNestedProperty('header.append_url_extension') ?: Utils::getPageFormat(), '.'); + } + ); + } + + /** + * Gets and sets the extension field. + * + * @param string|null $var + * @return string + */ + public function extension($var = null): string + { + if (null !== $var) { + $this->setProperty('format', $var); + } + + $language = $this->language(); + if ($language) { + $language = '.' . $language; + } + $format = '.' . ($this->getProperty('format') ?? pathinfo($this->name(), PATHINFO_EXTENSION)); + + return $language . $format; + } + + /** + * Gets and sets the expires field. If not set will return the default + * + * @param int|null $var The new expires value. + * @return int The expires value + */ + public function expires($var = null): int + { + return $this->loadHeaderProperty( + 'expires', + $var, + static function ($value) { + return (int)($value ?? Grav::instance()['config']->get('system.pages.expires')); + } + ); + } + + /** + * Gets and sets the cache-control property. If not set it will return the default value (null) + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details on valid options + * + * @param string|null $var + * @return string|null + */ + public function cacheControl($var = null): ?string + { + return $this->loadHeaderProperty( + 'cache_control', + $var, + static function ($value) { + return ((string)($value ?? Grav::instance()['config']->get('system.pages.cache_control'))) ?: null; + } + ); + } + + /** + * @param bool|null $var + * @return bool|null + */ + public function ssl($var = null): ?bool + { + return $this->loadHeaderProperty( + 'ssl', + $var, + static function ($value) { + return $value ? (bool)$value : null; + } + ); + } + + /** + * Returns the state of the debugger override setting for this page + * + * @return bool + */ + public function debugger(): bool + { + return (bool)$this->getNestedProperty('header.debugger', true); + } + + /** + * Function to merge page metadata tags and build an array of Metadata objects + * that can then be rendered in the page. + * + * @param array|null $var an Array of metadata values to set + * @return array an Array of metadata values for the page + */ + public function metadata($var = null): array + { + if ($var !== null) { + $this->_metadata = (array)$var; + } + + // if not metadata yet, process it. + if (null === $this->_metadata) { + $this->_metadata = []; + + $config = Grav::instance()['config']; + + // Set the Generator tag + $defaultMetadata = ['generator' => 'GravCMS']; + $siteMetadata = $config->get('site.metadata', []); + $headerMetadata = $this->getNestedProperty('header.metadata', []); + + // Get initial metadata for the page + $metadata = array_merge($defaultMetadata, $siteMetadata, $headerMetadata); + + $header_tag_http_equivs = ['content-type', 'default-style', 'refresh', 'x-ua-compatible', 'content-security-policy']; + $escape = !$config->get('system.strict_mode.twig_compat', false) || $config->get('system.twig.autoescape', true); + + // Build an array of meta objects.. + foreach ($metadata as $key => $value) { + // Lowercase the key + $key = strtolower($key); + + // If this is a property type metadata: "og", "twitter", "facebook" etc + // Backward compatibility for nested arrays in metas + if (is_array($value)) { + foreach ($value as $property => $prop_value) { + $prop_key = $key . ':' . $property; + $this->_metadata[$prop_key] = [ + 'name' => $prop_key, + 'property' => $prop_key, + 'content' => $escape ? htmlspecialchars($prop_value, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $prop_value + ]; + } + } elseif ($value) { + // If it this is a standard meta data type + if (in_array($key, $header_tag_http_equivs, true)) { + $this->_metadata[$key] = [ + 'http_equiv' => $key, + 'content' => $escape ? htmlspecialchars($value, ENT_COMPAT, 'UTF-8') : $value + ]; + } elseif ($key === 'charset') { + $this->_metadata[$key] = ['charset' => $escape ? htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $value]; + } else { + // if it's a social metadata with separator, render as property + $separator = strpos($key, ':'); + $hasSeparator = $separator && $separator < strlen($key) - 1; + $entry = [ + 'content' => $escape ? htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $value + ]; + + if ($hasSeparator && !Utils::startsWith($key, 'twitter')) { + $entry['property'] = $key; + } else { + $entry['name'] = $key; + } + + $this->_metadata[$key] = $entry; + } + } + } + } + + return $this->_metadata; + } + + /** + * Reset the metadata and pull from header again + */ + public function resetMetadata(): void + { + $this->_metadata = null; + } + + /** + * Gets and sets the option to show the etag header for the page. + * + * @param bool|null $var show etag header + * @return bool show etag header + */ + public function eTag($var = null): bool + { + return $this->loadHeaderProperty( + 'etag', + $var, + static function ($value) { + return (bool)($value ?? Grav::instance()['config']->get('system.pages.etag')); + } + ); + } + + /** + * Gets and sets the path to the .md file for this Page object. + * + * @param string|null $var the file path + * @return string|null the file path + */ + public function filePath($var = null): ?string + { + if (null !== $var) { + // TODO: + throw new RuntimeException(__METHOD__ . '(string): Not Implemented'); + } + + $folder = $this->getStorageFolder(); + if (!$folder) { + return null; + } + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $folder = $locator->isStream($folder) ? $locator->getResource($folder) : GRAV_ROOT . "/{$folder}"; + + return $folder . '/' . ($this->isPage() ? $this->name() : 'default.md'); + } + + /** + * Gets the relative path to the .md file + * + * @return string|null The relative file path + */ + public function filePathClean(): ?string + { + $folder = $this->getStorageFolder(); + if (!$folder) { + return null; + } + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $folder = $locator->isStream($folder) ? $locator->getResource($folder, false) : $folder; + + return $folder . '/' . ($this->isPage() ? $this->name() : 'default.md'); + } + + /** + * Gets and sets the order by which any sub-pages should be sorted. + * + * @param string|null $var the order, either "asc" or "desc" + * @return string the order, either "asc" or "desc" + */ + public function orderDir($var = null): string + { + return $this->loadHeaderProperty( + 'order_dir', + $var, + static function ($value) { + return strtolower(trim($value) ?: Grav::instance()['config']->get('system.pages.order.dir')) === 'desc' ? 'desc' : 'asc'; + } + ); + } + + /** + * Gets and sets the order by which the sub-pages should be sorted. + * + * default - is the order based on the file system, ie 01.Home before 02.Advark + * title - is the order based on the title set in the pages + * date - is the order based on the date set in the pages + * folder - is the order based on the name of the folder with any numerics omitted + * + * @param string|null $var supported options include "default", "title", "date", and "folder" + * @return string supported options include "default", "title", "date", and "folder" + */ + public function orderBy($var = null): string + { + return $this->loadHeaderProperty( + 'order_by', + $var, + static function ($value) { + return trim($value) ?: Grav::instance()['config']->get('system.pages.order.by'); + } + ); + } + + /** + * Gets the manual order set in the header. + * + * @param string|null $var supported options include "default", "title", "date", and "folder" + * @return array + */ + public function orderManual($var = null): array + { + return $this->loadHeaderProperty( + 'order_manual', + $var, + static function ($value) { + return (array)$value; + } + ); + } + + /** + * Gets and sets the maxCount field which describes how many sub-pages should be displayed if the + * sub_pages header property is set for this page object. + * + * @param int|null $var the maximum number of sub-pages + * @return int the maximum number of sub-pages + */ + public function maxCount($var = null): int + { + return $this->loadHeaderProperty( + 'max_count', + $var, + static function ($value) { + return (int)($value ?? Grav::instance()['config']->get('system.pages.list.count')); + } + ); + } + + /** + * Gets and sets the modular var that helps identify this page is a modular child + * + * @param bool|null $var true if modular_twig + * @return bool true if modular_twig + * @deprecated 1.7 Use ->isModule() or ->modularTwig() method instead. + */ + public function modular($var = null): bool + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, use ->isModule() or ->modularTwig() method instead', E_USER_DEPRECATED); + + return $this->modularTwig($var); + } + + /** + * Gets and sets the modular_twig var that helps identify this page as a modular child page that will need + * twig processing handled differently from a regular page. + * + * @param bool|null $var true if modular_twig + * @return bool true if modular_twig + */ + public function modularTwig($var = null): bool + { + if ($var !== null) { + $this->setProperty('modular_twig', (bool)$var); + if ($var) { + $this->visible(false); + } + } + + return (bool)($this->getProperty('modular_twig') ?? strpos($this->slug(), '_') === 0); + } + + /** + * Returns children of this page. + * + * @return PageCollectionInterface|FlexIndexInterface + */ + public function children() + { + $meta = $this->getMetaData(); + $keys = array_keys($meta['children'] ?? []); + $prefix = $this->getMasterKey(); + if ($prefix) { + foreach ($keys as &$key) { + $key = $prefix . '/' . $key; + } + unset($key); + } + + return $this->getFlexDirectory()->getIndex($keys, 'storage_key'); + } + + /** + * Check to see if this item is the first in an array of sub-pages. + * + * @return bool True if item is first. + */ + public function isFirst(): bool + { + $parent = $this->parent(); + $children = $parent ? $parent->children() : null; + if ($children instanceof FlexCollectionInterface) { + $children = $children->withKeyField(); + } + + return $children instanceof PageCollectionInterface ? $children->isFirst($this->getKey()) : true; + } + + /** + * Check to see if this item is the last in an array of sub-pages. + * + * @return bool True if item is last + */ + public function isLast(): bool + { + $parent = $this->parent(); + $children = $parent ? $parent->children() : null; + if ($children instanceof FlexCollectionInterface) { + $children = $children->withKeyField(); + } + + return $children instanceof PageCollectionInterface ? $children->isLast($this->getKey()) : true; + } + + /** + * Gets the previous sibling based on current position. + * + * @return PageInterface|false the previous Page item + */ + public function prevSibling() + { + return $this->adjacentSibling(-1); + } + + /** + * Gets the next sibling based on current position. + * + * @return PageInterface|false the next Page item + */ + public function nextSibling() + { + return $this->adjacentSibling(1); + } + + /** + * Returns the adjacent sibling based on a direction. + * + * @param int $direction either -1 or +1 + * @return PageInterface|false the sibling page + */ + public function adjacentSibling($direction = 1) + { + $parent = $this->parent(); + $children = $parent ? $parent->children() : null; + if ($children instanceof FlexCollectionInterface) { + $children = $children->withKeyField(); + } + + if ($children instanceof PageCollectionInterface) { + $child = $children->adjacentSibling($this->getKey(), $direction); + if ($child instanceof PageInterface) { + return $child; + } + } + + return false; + } + + /** + * Helper method to return an ancestor page. + * + * @param string|null $lookup Name of the parent folder + * @return PageInterface|null page you were looking for if it exists + */ + public function ancestor($lookup = null) + { + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->ancestor($this->getProperty('parent_route'), $lookup); + } + + /** + * Helper method to return an ancestor page to inherit from. The current + * page object is returned. + * + * @param string $field Name of the parent folder + * @return PageInterface|null + */ + public function inherited($field) + { + [$inherited, $currentParams] = $this->getInheritedParams($field); + + $this->modifyHeader($field, $currentParams); + + return $inherited; + } + + /** + * Helper method to return an ancestor field only to inherit from. The + * first occurrence of an ancestor field will be returned if at all. + * + * @param string $field Name of the parent folder + * @return array + */ + public function inheritedField($field): array + { + [, $currentParams] = $this->getInheritedParams($field); + + return $currentParams; + } + + /** + * Method that contains shared logic for inherited() and inheritedField() + * + * @param string $field Name of the parent folder + * @return array + */ + protected function getInheritedParams($field): array + { + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + $inherited = $pages->inherited($this->getProperty('parent_route'), $field); + $inheritedParams = $inherited ? (array)$inherited->value('header.' . $field) : []; + $currentParams = (array)$this->getFormValue('header.' . $field); + if ($inheritedParams && is_array($inheritedParams)) { + $currentParams = array_replace_recursive($inheritedParams, $currentParams); + } + + return [$inherited, $currentParams]; + } + + /** + * Helper method to return a page. + * + * @param string $url the url of the page + * @param bool $all + * @return PageInterface|null page you were looking for if it exists + */ + public function find($url, $all = false) + { + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->find($url, $all); + } + + /** + * Get a collection of pages in the current context. + * + * @param string|array $params + * @param bool $pagination + * @return PageCollectionInterface|Collection + * @throws InvalidArgumentException + */ + public function collection($params = 'content', $pagination = true) + { + if (is_string($params)) { + // Look into a page header field. + $params = (array)$this->getFormValue('header.' . $params); + } elseif (!is_array($params)) { + throw new InvalidArgumentException('Argument should be either header variable name or array of parameters'); + } + + if (!$pagination) { + $params['pagination'] = false; + } + $context = [ + 'pagination' => $pagination, + 'self' => $this + ]; + + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->getCollection($params, $context); + } + + /** + * @param string|array $value + * @param bool $only_published + * @return PageCollectionInterface|Collection + */ + public function evaluate($value, $only_published = true) + { + $params = [ + 'items' => $value, + 'published' => $only_published + ]; + $context = [ + 'event' => false, + 'pagination' => false, + 'url_taxonomy_filters' => false, + 'self' => $this + ]; + + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + + return $pages->getCollection($params, $context); + } + + /** + * Returns whether or not the current folder exists + * + * @return bool + */ + public function folderExists(): bool + { + return $this->exists() || is_dir($this->getStorageFolder() ?? ''); + } + + /** + * Gets the action. + * + * @return string|null The Action string. + */ + public function getAction(): ?string + { + $meta = $this->getMetaData(); + if (!empty($meta['copy'])) { + return 'copy'; + } + if (isset($meta['storage_key']) && $this->getStorageKey() !== $meta['storage_key']) { + return 'move'; + } + + return null; + } +} diff --git a/system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php b/system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php new file mode 100644 index 0000000..90773cd --- /dev/null +++ b/system/src/Grav/Framework/Flex/Pages/Traits/PageRoutableTrait.php @@ -0,0 +1,550 @@ +loadHeaderProperty( + 'url_extension', + null, + function ($value) { + if ($this->home()) { + return ''; + } + + return $value ?? Grav::instance()['config']->get('system.pages.append_url_extension', ''); + } + ); + } + + /** + * Gets and Sets whether or not this Page is routable, ie you can reach it via a URL. + * The page must be *routable* and *published* + * + * @param bool|null $var true if the page is routable + * @return bool true if the page is routable + */ + public function routable($var = null): bool + { + $value = $this->loadHeaderProperty( + 'routable', + $var, + static function ($value) { + return $value ?? true; + } + ); + + return $value && $this->published() && !$this->isModule() && !$this->root() && $this->getLanguages(true); + } + + /** + * Gets the URL for a page - alias of url(). + * + * @param bool $include_host + * @return string the permalink + */ + public function link($include_host = false): string + { + return $this->url($include_host); + } + + /** + * Gets the URL with host information, aka Permalink. + * @return string The permalink. + */ + public function permalink(): string + { + return $this->url(true, false, true, true); + } + + /** + * Returns the canonical URL for a page + * + * @param bool $include_lang + * @return string + */ + public function canonical($include_lang = true): string + { + return $this->url(true, true, $include_lang); + } + + /** + * Gets the url for the Page. + * + * @param bool $include_host Defaults false, but true would include http://yourhost.com + * @param bool $canonical true to return the canonical URL + * @param bool $include_base + * @param bool $raw_route + * @return string The url. + */ + public function url($include_host = false, $canonical = false, $include_base = true, $raw_route = false): string + { + // Override any URL when external_url is set + $external = $this->getNestedProperty('header.external_url'); + if ($external) { + return $external; + } + + $grav = Grav::instance(); + + /** @var Pages $pages */ + $pages = $grav['pages']; + + /** @var Config $config */ + $config = $grav['config']; + + // get base route (multi-site base and language) + $route = $include_base ? $pages->baseRoute() : ''; + + // add full route if configured to do so + if (!$include_host && $config->get('system.absolute_urls', false)) { + $include_host = true; + } + + if ($canonical) { + $route .= $this->routeCanonical(); + } elseif ($raw_route) { + $route .= $this->rawRoute(); + } else { + $route .= $this->route(); + } + + /** @var Uri $uri */ + $uri = $grav['uri']; + $url = $uri->rootUrl($include_host) . '/' . trim($route, '/') . $this->urlExtension(); + + return Uri::filterPath($url); + } + + /** + * Gets the route for the page based on the route headers if available, else from + * the parents route and the current Page's slug. + * + * @param string $var Set new default route. + * @return string|null The route for the Page. + */ + public function route($var = null): ?string + { + if (null !== $var) { + // TODO: not the best approach, but works... + $this->setNestedProperty('header.routes.default', $var); + } + + // Return default route if given. + $default = $this->getNestedProperty('header.routes.default'); + if (is_string($default)) { + return $default; + } + + return $this->routeInternal(); + } + + /** + * @return string|null + */ + protected function routeInternal(): ?string + { + $route = $this->_route; + if (null !== $route) { + return $route; + } + + if ($this->root()) { + return null; + } + + // Root and orphan nodes have no route. + $parent = $this->parent(); + if (!$parent) { + return null; + } + + if ($parent->home()) { + /** @var Config $config */ + $config = Grav::instance()['config']; + $hide = (bool)$config->get('system.home.hide_in_urls', false); + $route = '/' . ($hide ? '' : $parent->slug()); + } else { + $route = $parent->route(); + } + + if ($route !== '' && $route !== '/') { + $route .= '/'; + } + + if (!$this->home()) { + $route .= $this->slug(); + } + + $this->_route = $route; + + return $route; + } + + /** + * Helper method to clear the route out so it regenerates next time you use it + */ + public function unsetRouteSlug(): void + { + // TODO: + throw new RuntimeException(__METHOD__ . '(): Not Implemented'); + } + + /** + * Gets and Sets the page raw route + * + * @param string|null $var + * @return string|null + */ + public function rawRoute($var = null): ?string + { + if (null !== $var) { + // TODO: + throw new RuntimeException(__METHOD__ . '(string): Not Implemented'); + } + + if ($this->root()) { + return null; + } + + return '/' . $this->getKey(); + } + + /** + * Gets the route aliases for the page based on page headers. + * + * @param array|null $var list of route aliases + * @return array The route aliases for the Page. + */ + public function routeAliases($var = null): array + { + if (null !== $var) { + $this->setNestedProperty('header.routes.aliases', (array)$var); + } + + $aliases = (array)$this->getNestedProperty('header.routes.aliases'); + $default = $this->getNestedProperty('header.routes.default'); + if ($default) { + $aliases[] = $default; + } + + return $aliases; + } + + /** + * Gets the canonical route for this page if its set. If provided it will use + * that value, else if it's `true` it will use the default route. + * + * @param string|null $var + * @return string|null + */ + public function routeCanonical($var = null): ?string + { + if (null !== $var) { + $this->setNestedProperty('header.routes.canonical', (array)$var); + } + + $canonical = $this->getNestedProperty('header.routes.canonical'); + + return is_string($canonical) ? $canonical : $this->route(); + } + + /** + * Gets the redirect set in the header. + * + * @param string|null $var redirect url + * @return string|null + */ + public function redirect($var = null): ?string + { + return $this->loadHeaderProperty( + 'redirect', + $var, + static function ($value) { + return trim($value) ?: null; + } + ); + } + + /** + * Returns the clean path to the page file + * + * Needed in admin for Page Media. + */ + public function relativePagePath(): ?string + { + $folder = $this->getMediaFolder(); + if (!$folder) { + return null; + } + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $path = $locator->isStream($folder) ? $locator->findResource($folder, false) : $folder; + + return is_string($path) ? $path : null; + } + + /** + * Gets and sets the path to the folder where the .md for this Page object resides. + * This is equivalent to the filePath but without the filename. + * + * @param string|null $var the path + * @return string|null the path + */ + public function path($var = null): ?string + { + if (null !== $var) { + // TODO: + throw new RuntimeException(__METHOD__ . '(string): Not Implemented'); + } + + $path = $this->_path; + if ($path) { + return $path; + } + + if ($this->root()) { + $folder = $this->getFlexDirectory()->getStorageFolder(); + } else { + $folder = $this->getStorageFolder(); + } + + if ($folder) { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $folder = $locator->isStream($folder) ? $locator->getResource($folder) : GRAV_ROOT . "/{$folder}"; + } + + return $this->_path = is_string($folder) ? $folder : null; + } + + /** + * Get/set the folder. + * + * @param string|null $var Optional path, including numeric prefix. + * @return string|null + */ + public function folder($var = null): ?string + { + return $this->loadProperty( + 'folder', + $var, + function ($value) { + if (null === $value) { + $value = $this->getMasterKey() ?: $this->getKey(); + } + + return basename($value) ?: null; + } + ); + } + + /** + * Get/set the folder. + * + * @param string|null $var Optional path, including numeric prefix. + * @return string|null + */ + public function parentStorageKey($var = null): ?string + { + return $this->loadProperty( + 'parent_key', + $var, + function ($value) { + if (null === $value) { + $filesystem = Filesystem::getInstance(false); + $value = $this->getMasterKey() ?: $this->getKey(); + $value = ltrim($filesystem->dirname("/{$value}"), '/') ?: ''; + } + + return $value; + } + ); + } + + /** + * Gets and Sets the parent object for this page + * + * @param PageInterface|null $var the parent page object + * @return PageInterface|null the parent page object if it exists. + */ + public function parent(PageInterface $var = null) + { + if (null !== $var) { + // TODO: + throw new RuntimeException(__METHOD__ . '(PageInterface): Not Implemented'); + } + + if ($this->_parentCache || $this->root()) { + return $this->_parentCache; + } + + // Use filesystem as \dirname() does not work in Windows because of '/foo' becomes '\'. + $filesystem = Filesystem::getInstance(false); + $directory = $this->getFlexDirectory(); + $parentKey = ltrim($filesystem->dirname("/{$this->getKey()}"), '/'); + if ('' !== $parentKey) { + $parent = $directory->getObject($parentKey); + $language = $this->getLanguage(); + if ($language && $parent && method_exists($parent, 'getTranslation')) { + $parent = $parent->getTranslation($language) ?? $parent; + } + + $this->_parentCache = $parent; + } else { + $index = $directory->getIndex(); + + $this->_parentCache = \is_callable([$index, 'getRoot']) ? $index->getRoot() : null; + } + + return $this->_parentCache; + } + + /** + * Gets the top parent object for this page. Can return page itself. + * + * @return PageInterface The top parent page object. + */ + public function topParent() + { + $topParent = $this; + while ($topParent) { + $parent = $topParent->parent(); + if (!$parent || !$parent->parent()) { + break; + } + $topParent = $parent; + } + + return $topParent; + } + + /** + * Returns the item in the current position. + * + * @return int|null the index of the current page. + */ + public function currentPosition(): ?int + { + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; + if ($collection instanceof PageCollectionInterface && $path = $this->path()) { + return $collection->currentPosition($path); + } + + return 1; + } + + /** + * Returns whether or not this page is the currently active page requested via the URL. + * + * @return bool True if it is active + */ + public function active(): bool + { + $grav = Grav::instance(); + $uri_path = rtrim(urldecode($grav['uri']->path()), '/') ?: '/'; + $routes = $grav['pages']->routes(); + + return isset($routes[$uri_path]) && $routes[$uri_path] === $this->path(); + } + + /** + * Returns whether or not this URI's URL contains the URL of the active page. + * Or in other words, is this page's URL in the current URL + * + * @return bool True if active child exists + */ + public function activeChild(): bool + { + $grav = Grav::instance(); + /** @var Uri $uri */ + $uri = $grav['uri']; + /** @var Pages $pages */ + $pages = $grav['pages']; + $uri_path = rtrim(urldecode($uri->path()), '/'); + $routes = $pages->routes(); + + if (isset($routes[$uri_path])) { + $page = $pages->find($uri->route()); + /** @var PageInterface|null $child_page */ + $child_page = $page ? $page->parent() : null; + while ($child_page && !$child_page->root()) { + if ($this->path() === $child_page->path()) { + return true; + } + $child_page = $child_page->parent(); + } + } + + return false; + } + + /** + * Returns whether or not this page is the currently configured home page. + * + * @return bool True if it is the homepage + */ + public function home(): bool + { + $home = Grav::instance()['config']->get('system.home.alias'); + + return '/' . $this->getKey() === $home; + } + + /** + * Returns whether or not this page is the root node of the pages tree. + * + * @param bool|null $var + * @return bool True if it is the root + */ + public function root($var = null): bool + { + if (null !== $var) { + $this->root = (bool)$var; + } + + return $this->root === true || $this->getKey() === '/'; + } +} diff --git a/system/src/Grav/Framework/Flex/Pages/Traits/PageTranslateTrait.php b/system/src/Grav/Framework/Flex/Pages/Traits/PageTranslateTrait.php new file mode 100644 index 0000000..877fec1 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Pages/Traits/PageTranslateTrait.php @@ -0,0 +1,283 @@ +findTranslation($languageCode, $fallback); + + return null !== $code; + } + + /** + * @param string|null $languageCode + * @param bool|null $fallback + * @return FlexObjectInterface|PageInterface|null + */ + public function getTranslation(string $languageCode = null, bool $fallback = null) + { + if ($this->root()) { + return $this; + } + + $code = $this->findTranslation($languageCode, $fallback); + if (null === $code) { + $object = null; + } elseif ('' === $code) { + $object = $this->getLanguage() ? $this->getFlexDirectory()->getObject($this->getMasterKey(), 'storage_key') : $this; + } else { + $meta = $this->getMetaData(); + $meta['template'] = $this->getLanguageTemplates()[$code] ?? $meta['template']; + $key = $this->getStorageKey() . '|' . $meta['template'] . '.' . $code; + $meta['storage_key'] = $key; + $meta['lang'] = $code; + $object = $this->getFlexDirectory()->loadObjects([$key => $meta])[$key] ?? null; + } + + return $object; + } + + /** + * @param bool $includeDefault If set to true, return separate entries for '' and 'en' (default) language. + * @return array + */ + public function getAllLanguages(bool $includeDefault = false): array + { + $grav = Grav::instance(); + + /** @var Language $language */ + $language = $grav['language']; + $languages = $language->getLanguages(); + if (!$languages) { + return []; + } + + $translated = $this->getLanguageTemplates(); + + if ($includeDefault) { + $languages[] = ''; + } elseif (isset($translated[''])) { + $default = $language->getDefault(); + if (is_bool($default)) { + $default = ''; + } + $translated[$default] = $translated['']; + unset($translated['']); + } + + $languages = array_fill_keys($languages, false); + $translated = array_fill_keys(array_keys($translated), true); + + return array_replace($languages, $translated); + } + + /** + * Returns all translated languages. + * + * @param bool $includeDefault If set to true, return separate entries for '' and 'en' (default) language. + * @return array + */ + public function getLanguages(bool $includeDefault = false): array + { + $languages = $this->getLanguageTemplates(); + + if (!$includeDefault && isset($languages[''])) { + $grav = Grav::instance(); + + /** @var Language $language */ + $language = $grav['language']; + $default = $language->getDefault(); + if (is_bool($default)) { + $default = ''; + } + $languages[$default] = $languages['']; + unset($languages['']); + } + + return array_keys($languages); + } + + /** + * @return string + */ + public function getLanguage(): string + { + return $this->language() ?? ''; + } + + /** + * @param string|null $languageCode + * @param bool|null $fallback + * @return string|null + */ + public function findTranslation(string $languageCode = null, bool $fallback = null): ?string + { + $translated = $this->getLanguageTemplates(); + + // If there's no translations (including default), we have an empty folder. + if (!$translated) { + return ''; + } + + // FIXME: only published is not implemented... + $languages = $this->getFallbackLanguages($languageCode, $fallback); + + $language = null; + foreach ($languages as $code) { + if (isset($translated[$code])) { + $language = $code; + break; + } + } + + return $language; + } + + /** + * Return an array with the routes of other translated languages + * + * @param bool $onlyPublished only return published translations + * @return array the page translated languages + */ + public function translatedLanguages($onlyPublished = false): array + { + // FIXME: only published is not implemented... + $translated = $this->getLanguageTemplates(); + if (!$translated) { + return $translated; + } + + $grav = Grav::instance(); + + /** @var Language $language */ + $language = $grav['language']; + $languages = $language->getLanguages(); + $languages[] = ''; + + $translated = array_intersect_key($translated, array_flip($languages)); + $list = array_fill_keys($languages, null); + foreach ($translated as $languageCode => $languageFile) { + $path = ($languageCode ? '/' : '') . $languageCode; + $list[$languageCode] = "{$path}/{$this->getKey()}"; + } + + return array_filter($list); + } + + /** + * Return an array listing untranslated languages available + * + * @param bool $includeUnpublished also list unpublished translations + * @return array the page untranslated languages + */ + public function untranslatedLanguages($includeUnpublished = false): array + { + $grav = Grav::instance(); + + /** @var Language $language */ + $language = $grav['language']; + + $languages = $language->getLanguages(); + $translated = array_keys($this->translatedLanguages(!$includeUnpublished)); + + return array_values(array_diff($languages, $translated)); + } + + /** + * Get page language + * + * @param string|null $var + * @return string|null + */ + public function language($var = null): ?string + { + return $this->loadHeaderProperty( + 'lang', + $var, + function ($value) { + $value = $value ?? $this->getMetaData()['lang'] ?? ''; + + return trim($value) ?: null; + } + ); + } + + /** + * @return array + */ + protected function getLanguageTemplates(): array + { + if (null === $this->_languages) { + $template = $this->getProperty('template'); + $meta = $this->getMetaData(); + $translations = $meta['markdown'] ?? []; + $list = []; + foreach ($translations as $code => $search) { + if (isset($search[$template])) { + // Use main template if possible. + $list[$code] = $template; + } elseif (!empty($search)) { + // Fall back to first matching template. + $list[$code] = key($search); + } + } + + $this->_languages = $list; + } + + return $this->_languages; + } + + /** + * @param string|null $languageCode + * @param bool|null $fallback + * @return array + */ + protected function getFallbackLanguages(string $languageCode = null, bool $fallback = null): array + { + $fallback = $fallback ?? true; + if (!$fallback && null !== $languageCode) { + return [$languageCode]; + } + + $grav = Grav::instance(); + + /** @var Language $language */ + $language = $grav['language']; + $languageCode = $languageCode ?? ($language->getLanguage() ?: ''); + if ($languageCode === '' && $fallback) { + return $language->getFallbackLanguages(null, true); + } + + return $fallback ? $language->getFallbackLanguages($languageCode, true) : [$languageCode]; + } +} diff --git a/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php b/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php new file mode 100644 index 0000000..1934f50 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php @@ -0,0 +1,228 @@ +hasKey((string)$key); + } + + return $list; + } + + /** + * {@inheritDoc} + * @see FlexStorageInterface::getKeyField() + */ + public function getKeyField(): string + { + return $this->keyField; + } + + /** + * @param array $keys + * @param bool $includeParams + * @return string + */ + public function buildStorageKey(array $keys, bool $includeParams = true): string + { + $key = $keys['key'] ?? ''; + $params = $includeParams ? $this->buildStorageKeyParams($keys) : ''; + + return $params ? "{$key}|{$params}" : $key; + } + + /** + * @param array $keys + * @return string + */ + public function buildStorageKeyParams(array $keys): string + { + return ''; + } + + /** + * @param array $row + * @return array + */ + public function extractKeysFromRow(array $row): array + { + return [ + 'key' => $this->normalizeKey($row[$this->keyField] ?? '') + ]; + } + + /** + * @param string $key + * @return array + */ + public function extractKeysFromStorageKey(string $key): array + { + return [ + 'key' => $key + ]; + } + + /** + * @param string|array $formatter + * @return void + */ + protected function initDataFormatter($formatter): void + { + // Initialize formatter. + if (!is_array($formatter)) { + $formatter = ['class' => $formatter]; + } + $formatterClassName = $formatter['class'] ?? JsonFormatter::class; + $formatterOptions = $formatter['options'] ?? []; + + $this->dataFormatter = new $formatterClassName($formatterOptions); + } + + /** + * @param string $filename + * @return string|null + */ + protected function detectDataFormatter(string $filename): ?string + { + if (preg_match('|(\.[a-z0-9]*)$|ui', $filename, $matches)) { + switch ($matches[1]) { + case '.json': + return JsonFormatter::class; + case '.yaml': + return YamlFormatter::class; + case '.md': + return MarkdownFormatter::class; + } + } + + return null; + } + + /** + * @param string $filename + * @return CompiledJsonFile|CompiledYamlFile|CompiledMarkdownFile + */ + protected function getFile(string $filename) + { + $filename = $this->resolvePath($filename); + + // TODO: start using the new file classes. + switch ($this->dataFormatter->getDefaultFileExtension()) { + case '.json': + $file = CompiledJsonFile::instance($filename); + break; + case '.yaml': + $file = CompiledYamlFile::instance($filename); + break; + case '.md': + $file = CompiledMarkdownFile::instance($filename); + break; + default: + throw new RuntimeException('Unknown extension type ' . $this->dataFormatter->getDefaultFileExtension()); + } + + return $file; + } + + /** + * @param string $path + * @return string + */ + protected function resolvePath(string $path): string + { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + if (!$locator->isStream($path)) { + return GRAV_ROOT . "/{$path}"; + } + + return $locator->getResource($path); + } + + /** + * Generates a random, unique key for the row. + * + * @return string + */ + protected function generateKey(): string + { + return substr(hash('sha256', random_bytes($this->keyLen)), 0, $this->keyLen); + } + + /** + * @param string $key + * @return string + */ + public function normalizeKey(string $key): string + { + if ($this->caseSensitive === true) { + return $key; + } + + return mb_strtolower($key); + } + + /** + * Checks if a key is valid. + * + * @param string $key + * @return bool + */ + protected function validateKey(string $key): bool + { + return $key && (bool) preg_match('/^[^\\/?*:;{}\\\\\\n]+$/u', $key); + } +} diff --git a/system/src/Grav/Framework/Flex/Storage/FileStorage.php b/system/src/Grav/Framework/Flex/Storage/FileStorage.php new file mode 100644 index 0000000..eabd658 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Storage/FileStorage.php @@ -0,0 +1,159 @@ +dataPattern = '{FOLDER}/{KEY}{EXT}'; + + if (!isset($options['formatter']) && isset($options['pattern'])) { + $options['formatter'] = $this->detectDataFormatter($options['pattern']); + } + + parent::__construct($options); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getMediaPath() + */ + public function getMediaPath(string $key = null): ?string + { + $path = $this->getStoragePath(); + if (!$path) { + return null; + } + + return $key ? "{$path}/{$key}" : $path; + } + + /** + * @param string $src + * @param string $dst + * @return bool + */ + public function copyRow(string $src, string $dst): bool + { + if ($this->hasKey($dst)) { + throw new RuntimeException("Cannot copy object: key '{$dst}' is already taken"); + } + + if (!$this->hasKey($src)) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::renameRow() + */ + public function renameRow(string $src, string $dst): bool + { + if (!$this->hasKey($src)) { + return false; + } + + // Remove old file. + $path = $this->getPathFromKey($src); + $file = $this->getFile($path); + $file->delete(); + $file->free(); + unset($file); + + return true; + } + + /** + * @param string $src + * @param string $dst + * @return bool + */ + protected function copyFolder(string $src, string $dst): bool + { + // Nothing to copy. + return true; + } + + /** + * @param string $src + * @param string $dst + * @return bool + */ + protected function moveFolder(string $src, string $dst): bool + { + // Nothing to move. + return true; + } + + /** + * @param string $key + * @return bool + */ + protected function canDeleteFolder(string $key): bool + { + return false; + } + + /** + * {@inheritdoc} + */ + protected function getKeyFromPath(string $path): string + { + return basename($path, $this->dataFormatter->getDefaultFileExtension()); + } + + /** + * {@inheritdoc} + */ + protected function buildIndex(): array + { + $this->clearCache(); + + $path = $this->getStoragePath(); + if (!$path || !file_exists($path)) { + return []; + } + + $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS; + $iterator = new FilesystemIterator($path, $flags); + $list = []; + /** @var SplFileInfo $info */ + foreach ($iterator as $filename => $info) { + if (!$info->isFile() || !($key = $this->getKeyFromPath($filename)) || strpos($info->getFilename(), '.') === 0) { + continue; + } + + $list[$key] = $this->getObjectMeta($key); + } + + ksort($list, SORT_NATURAL | SORT_FLAG_CASE); + + return $list; + } +} diff --git a/system/src/Grav/Framework/Flex/Storage/FolderStorage.php b/system/src/Grav/Framework/Flex/Storage/FolderStorage.php new file mode 100644 index 0000000..b2b9533 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Storage/FolderStorage.php @@ -0,0 +1,708 @@ + '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s']; + /** @var string Filename for the object. */ + protected $dataFile; + /** @var string File extension for the object. */ + protected $dataExt; + /** @var bool */ + protected $prefixed; + /** @var bool */ + protected $indexed; + /** @var array */ + protected $meta = []; + + /** + * {@inheritdoc} + */ + public function __construct(array $options) + { + if (!isset($options['folder'])) { + throw new InvalidArgumentException("Argument \$options is missing 'folder'"); + } + + $this->initDataFormatter($options['formatter'] ?? []); + $this->initOptions($options); + } + + /** + * @return bool + */ + public function isIndexed(): bool + { + return $this->indexed; + } + + /** + * @return void + */ + public function clearCache(): void + { + $this->meta = []; + } + + /** + * @param string[] $keys + * @param bool $reload + * @return array + */ + public function getMetaData(array $keys, bool $reload = false): array + { + $list = []; + foreach ($keys as $key) { + $list[$key] = $this->getObjectMeta((string)$key, $reload); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getExistingKeys() + */ + public function getExistingKeys(): array + { + return $this->buildIndex(); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::hasKey() + */ + public function hasKey(string $key): bool + { + $meta = $this->getObjectMeta($key); + + return array_key_exists('exists', $meta) ? $meta['exists'] : !empty($meta['storage_timestamp']); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::createRows() + */ + public function createRows(array $rows): array + { + $list = []; + foreach ($rows as $key => $row) { + $list[$key] = $this->saveRow('@@', $row); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::readRows() + */ + public function readRows(array $rows, array &$fetched = null): array + { + $list = []; + foreach ($rows as $key => $row) { + if (null === $row || is_scalar($row)) { + // Only load rows which haven't been loaded before. + $key = (string)$key; + $list[$key] = $this->loadRow($key); + + if (null !== $fetched) { + $fetched[$key] = $list[$key]; + } + } else { + // Keep the row if it has been loaded. + $list[$key] = $row; + } + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::updateRows() + */ + public function updateRows(array $rows): array + { + $list = []; + foreach ($rows as $key => $row) { + $key = (string)$key; + $list[$key] = $this->hasKey($key) ? $this->saveRow($key, $row) : null; + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::deleteRows() + */ + public function deleteRows(array $rows): array + { + $list = []; + $baseMediaPath = $this->getMediaPath(); + foreach ($rows as $key => $row) { + $key = (string)$key; + if (!$this->hasKey($key)) { + $list[$key] = null; + } else { + $path = $this->getPathFromKey($key); + $file = $this->getFile($path); + $list[$key] = $this->deleteFile($file); + + if ($this->canDeleteFolder($key)) { + $storagePath = $this->getStoragePath($key); + $mediaPath = $this->getMediaPath($key); + + if ($storagePath) { + $this->deleteFolder($storagePath, true); + } + if ($mediaPath && $mediaPath !== $storagePath && $mediaPath !== $baseMediaPath) { + $this->deleteFolder($mediaPath, true); + } + } + } + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::replaceRows() + */ + public function replaceRows(array $rows): array + { + $list = []; + foreach ($rows as $key => $row) { + $key = (string)$key; + $list[$key] = $this->saveRow($key, $row); + } + + return $list; + } + + /** + * @param string $src + * @param string $dst + * @return bool + * @throws RuntimeException + */ + public function copyRow(string $src, string $dst): bool + { + if ($this->hasKey($dst)) { + throw new RuntimeException("Cannot copy object: key '{$dst}' is already taken"); + } + + if (!$this->hasKey($src)) { + return false; + } + + $srcPath = $this->getStoragePath($src); + $dstPath = $this->getStoragePath($dst); + if (!$srcPath || !$dstPath) { + return false; + } + + return $this->copyFolder($srcPath, $dstPath); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::renameRow() + * @throws RuntimeException + */ + public function renameRow(string $src, string $dst): bool + { + if (!$this->hasKey($src)) { + return false; + } + + $srcPath = $this->getStoragePath($src); + $dstPath = $this->getStoragePath($dst); + if (!$srcPath || !$dstPath) { + throw new RuntimeException("Destination path '{$dst}' is empty"); + } + + if ($srcPath === $dstPath) { + return true; + } + + if ($this->hasKey($dst)) { + throw new RuntimeException("Cannot rename object '{$src}': key '{$dst}' is already taken $srcPath $dstPath"); + } + + return $this->moveFolder($srcPath, $dstPath); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getStoragePath() + */ + public function getStoragePath(string $key = null): ?string + { + if (null === $key || $key === '') { + $path = $this->dataFolder; + } else { + $parts = $this->parseKey($key, false); + $options = [ + $this->dataFolder, // {FOLDER} + $parts['key'], // {KEY} + $parts['key:2'], // {KEY:2} + '***', // {FILE} + '***' // {EXT} + ]; + + $path = rtrim(explode('***', sprintf($this->dataPattern, ...$options))[0], '/'); + } + + return $path; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getMediaPath() + */ + public function getMediaPath(string $key = null): ?string + { + return $this->getStoragePath($key); + } + + /** + * Get filesystem path from the key. + * + * @param string $key + * @return string + */ + public function getPathFromKey(string $key): string + { + $parts = $this->parseKey($key); + $options = [ + $this->dataFolder, // {FOLDER} + $parts['key'], // {KEY} + $parts['key:2'], // {KEY:2} + $parts['file'], // {FILE} + $this->dataExt // {EXT} + ]; + + return sprintf($this->dataPattern, ...$options); + } + + /** + * @param string $key + * @param bool $variations + * @return array + */ + public function parseKey(string $key, bool $variations = true): array + { + $keys = [ + 'key' => $key, + 'key:2' => mb_substr($key, 0, 2), + ]; + if ($variations) { + $keys['file'] = $this->dataFile; + } + + return $keys; + } + + /** + * Get key from the filesystem path. + * + * @param string $path + * @return string + */ + protected function getKeyFromPath(string $path): string + { + return basename($path); + } + + /** + * Prepares the row for saving and returns the storage key for the record. + * + * @param array $row + * @return void + */ + protected function prepareRow(array &$row): void + { + if (array_key_exists($this->keyField, $row)) { + $key = $row[$this->keyField]; + if ($key === $this->normalizeKey($key)) { + unset($row[$this->keyField]); + } + } + } + + /** + * @param string $key + * @return array + */ + protected function loadRow(string $key): ?array + { + $path = $this->getPathFromKey($key); + $file = $this->getFile($path); + try { + $data = (array)$file->content(); + if (isset($data[0])) { + throw new RuntimeException('Broken object file'); + } + + // Add key field to the object. + $keyField = $this->keyField; + if ($keyField !== 'storage_key' && !isset($data[$keyField])) { + $data[$keyField] = $key; + } + } catch (RuntimeException $e) { + $data = ['__ERROR' => $e->getMessage()]; + } finally { + $file->free(); + unset($file); + } + + $data['__META'] = $this->getObjectMeta($key); + + return $data; + } + + /** + * @param string $key + * @param array $row + * @return array + */ + protected function saveRow(string $key, array $row): array + { + try { + if (isset($row[$this->keyField])) { + $key = $row[$this->keyField]; + } + if (strpos($key, '@@') !== false) { + $key = $this->getNewKey(); + } + + $key = $this->normalizeKey($key); + + // Check if the row already exists and if the key has been changed. + $oldKey = $row['__META']['storage_key'] ?? null; + if (is_string($oldKey) && $oldKey !== $key) { + $isCopy = $row['__META']['copy'] ?? false; + if ($isCopy) { + $this->copyRow($oldKey, $key); + } else { + $this->renameRow($oldKey, $key); + } + } + + $this->prepareRow($row); + unset($row['__META'], $row['__ERROR']); + + $path = $this->getPathFromKey($key); + $file = $this->getFile($path); + + $file->save($row); + + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf('Flex saveFile(%s): %s', $path ?? $key, $e->getMessage())); + } finally { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $locator->clearCache(); + + if (isset($file)) { + $file->free(); + unset($file); + } + } + + $row['__META'] = $this->getObjectMeta($key, true); + + return $row; + } + + /** + * @param File $file + * @return array|string + */ + protected function deleteFile(File $file) + { + $filename = $file->filename(); + try { + $data = $file->content(); + if ($file->exists()) { + $file->delete(); + } + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf('Flex deleteFile(%s): %s', $filename, $e->getMessage())); + } finally { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $locator->clearCache(); + + $file->free(); + } + + return $data; + } + + /** + * @param string $src + * @param string $dst + * @return bool + */ + protected function copyFolder(string $src, string $dst): bool + { + try { + Folder::copy($this->resolvePath($src), $this->resolvePath($dst)); + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf('Flex copyFolder(%s, %s): %s', $src, $dst, $e->getMessage())); + } finally { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $locator->clearCache(); + } + + return true; + } + + /** + * @param string $src + * @param string $dst + * @return bool + */ + protected function moveFolder(string $src, string $dst): bool + { + try { + Folder::move($this->resolvePath($src), $this->resolvePath($dst)); + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf('Flex moveFolder(%s, %s): %s', $src, $dst, $e->getMessage())); + } finally { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $locator->clearCache(); + } + + return true; + } + + /** + * @param string $path + * @param bool $include_target + * @return bool + */ + protected function deleteFolder(string $path, bool $include_target = false): bool + { + try { + return Folder::delete($this->resolvePath($path), $include_target); + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf('Flex deleteFolder(%s): %s', $path, $e->getMessage())); + } finally { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $locator->clearCache(); + } + } + + /** + * @param string $key + * @return bool + */ + protected function canDeleteFolder(string $key): bool + { + return true; + } + + /** + * Returns list of all stored keys in [key => timestamp] pairs. + * + * @return array + */ + protected function buildIndex(): array + { + $this->clearCache(); + + $path = $this->getStoragePath(); + if (!$path || !file_exists($path)) { + return []; + } + + if ($this->prefixed) { + $list = $this->buildPrefixedIndexFromFilesystem($path); + } else { + $list = $this->buildIndexFromFilesystem($path); + } + + ksort($list, SORT_NATURAL | SORT_FLAG_CASE); + + return $list; + } + + /** + * @param string $key + * @param bool $reload + * @return array + */ + protected function getObjectMeta(string $key, bool $reload = false): array + { + if (!$reload && isset($this->meta[$key])) { + return $this->meta[$key]; + } + + if ($key && strpos($key, '@@') === false) { + $filename = $this->getPathFromKey($key); + $modified = is_file($filename) ? filemtime($filename) : 0; + } else { + $modified = 0; + } + + $meta = [ + 'storage_key' => $key, + 'storage_timestamp' => $modified + ]; + + $this->meta[$key] = $meta; + + return $meta; + } + + /** + * @param string $path + * @return array + */ + protected function buildIndexFromFilesystem($path) + { + $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS; + + $iterator = new FilesystemIterator($path, $flags); + $list = []; + /** @var SplFileInfo $info */ + foreach ($iterator as $filename => $info) { + if (!$info->isDir() || strpos($info->getFilename(), '.') === 0) { + continue; + } + + $key = $this->getKeyFromPath($filename); + $meta = $this->getObjectMeta($key); + if ($meta['storage_timestamp']) { + $list[$key] = $meta; + } + } + + return $list; + } + + /** + * @param string $path + * @return array + */ + protected function buildPrefixedIndexFromFilesystem($path) + { + $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS; + + $iterator = new FilesystemIterator($path, $flags); + $list = [[]]; + /** @var SplFileInfo $info */ + foreach ($iterator as $filename => $info) { + if (!$info->isDir() || strpos($info->getFilename(), '.') === 0) { + continue; + } + + $list[] = $this->buildIndexFromFilesystem($filename); + } + + return array_merge(...$list); + } + + /** + * @return string + */ + protected function getNewKey(): string + { + // Make sure that the file doesn't exist. + do { + $key = $this->generateKey(); + } while (file_exists($this->getPathFromKey($key))); + + return $key; + } + + /** + * @param array $options + * @return void + */ + protected function initOptions(array $options): void + { + $extension = $this->dataFormatter->getDefaultFileExtension(); + + /** @var string $pattern */ + $pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern; + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $folder = $options['folder']; + if ($locator->isStream($folder)) { + $folder = $locator->getResource($folder, false); + } + + $this->dataFolder = $folder; + $this->dataFile = $options['file'] ?? 'item'; + $this->dataExt = $extension; + if (mb_strpos($pattern, '{FILE}') === false && mb_strpos($pattern, '{EXT}') === false) { + if (isset($options['file'])) { + $pattern .= '/{FILE}{EXT}'; + } else { + $filesystem = Filesystem::getInstance(true); + $this->dataFile = basename($pattern, $extension); + $pattern = $filesystem->dirname($pattern) . '/{FILE}{EXT}'; + } + } + $this->prefixed = (bool)($options['prefixed'] ?? strpos($pattern, '/{KEY:2}/')); + $this->indexed = (bool)($options['indexed'] ?? false); + $this->keyField = $options['key'] ?? 'storage_key'; + $this->keyLen = (int)($options['key_len'] ?? 32); + $this->caseSensitive = (bool)($options['case_sensitive'] ?? true); + + $pattern = Utils::simpleTemplate($pattern, $this->variables); + if (!$pattern) { + throw new RuntimeException('Bad storage folder pattern'); + } + + $this->dataPattern = $pattern; + } +} diff --git a/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php b/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php new file mode 100644 index 0000000..6faac9d --- /dev/null +++ b/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php @@ -0,0 +1,506 @@ +detectDataFormatter($options['folder']); + $this->initDataFormatter($formatter); + + $filesystem = Filesystem::getInstance(true); + + $extension = $this->dataFormatter->getDefaultFileExtension(); + $pattern = basename($options['folder']); + + $this->dataPattern = basename($pattern, $extension) . $extension; + $this->dataFolder = $filesystem->dirname($options['folder']); + $this->keyField = $options['key'] ?? 'storage_key'; + $this->keyLen = (int)($options['key_len'] ?? 32); + $this->prefix = $options['prefix'] ?? null; + + // Make sure that the data folder exists. + if (!file_exists($this->dataFolder)) { + try { + Folder::create($this->dataFolder); + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf('Flex: %s', $e->getMessage())); + } + } + } + + /** + * @return void + */ + public function clearCache(): void + { + $this->data = null; + $this->modified = 0; + } + + /** + * @param string[] $keys + * @param bool $reload + * @return array + */ + public function getMetaData(array $keys, bool $reload = false): array + { + if (null === $this->data || $reload) { + $this->buildIndex(); + } + + $list = []; + foreach ($keys as $key) { + $list[$key] = $this->getObjectMeta((string)$key); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getExistingKeys() + */ + public function getExistingKeys(): array + { + return $this->buildIndex(); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::hasKey() + */ + public function hasKey(string $key): bool + { + if (null === $this->data) { + $this->buildIndex(); + } + + return $key && strpos($key, '@@') === false && isset($this->data[$key]); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::createRows() + */ + public function createRows(array $rows): array + { + if (null === $this->data) { + $this->buildIndex(); + } + + $list = []; + foreach ($rows as $key => $row) { + $list[$key] = $this->saveRow('@@', $rows); + } + + if ($list) { + $this->save(); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::readRows() + */ + public function readRows(array $rows, array &$fetched = null): array + { + if (null === $this->data) { + $this->buildIndex(); + } + + $list = []; + foreach ($rows as $key => $row) { + if (null === $row || is_scalar($row)) { + // Only load rows which haven't been loaded before. + $key = (string)$key; + $list[$key] = $this->hasKey($key) ? $this->loadRow($key) : null; + if (null !== $fetched) { + $fetched[$key] = $list[$key]; + } + } else { + // Keep the row if it has been loaded. + $list[$key] = $row; + } + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::updateRows() + */ + public function updateRows(array $rows): array + { + if (null === $this->data) { + $this->buildIndex(); + } + + $save = false; + $list = []; + foreach ($rows as $key => $row) { + $key = (string)$key; + if ($this->hasKey($key)) { + $list[$key] = $this->saveRow($key, $row); + $save = true; + } else { + $list[$key] = null; + } + } + + if ($save) { + $this->save(); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::deleteRows() + */ + public function deleteRows(array $rows): array + { + if (null === $this->data) { + $this->buildIndex(); + } + + $list = []; + foreach ($rows as $key => $row) { + $key = (string)$key; + if ($this->hasKey($key)) { + unset($this->data[$key]); + $list[$key] = $row; + } + } + + if ($list) { + $this->save(); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::replaceRows() + */ + public function replaceRows(array $rows): array + { + if (null === $this->data) { + $this->buildIndex(); + } + + $list = []; + foreach ($rows as $key => $row) { + $list[$key] = $this->saveRow((string)$key, $row); + } + + if ($list) { + $this->save(); + } + + return $list; + } + + /** + * @param string $src + * @param string $dst + * @return bool + */ + public function copyRow(string $src, string $dst): bool + { + if ($this->hasKey($dst)) { + throw new RuntimeException("Cannot copy object: key '{$dst}' is already taken"); + } + + if (!$this->hasKey($src)) { + return false; + } + + $this->data[$dst] = $this->data[$src]; + + return true; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::renameRow() + */ + public function renameRow(string $src, string $dst): bool + { + if (null === $this->data) { + $this->buildIndex(); + } + + if ($this->hasKey($dst)) { + throw new RuntimeException("Cannot rename object: key '{$dst}' is already taken"); + } + + if (!$this->hasKey($src)) { + return false; + } + + // Change single key in the array without changing the order or value. + $keys = array_keys($this->data); + $keys[array_search($src, $keys, true)] = $dst; + + $data = array_combine($keys, $this->data); + if (false === $data) { + throw new LogicException('Bad data'); + } + + $this->data = $data; + + return true; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getStoragePath() + */ + public function getStoragePath(string $key = null): ?string + { + return $this->dataFolder . '/' . $this->dataPattern; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getMediaPath() + */ + public function getMediaPath(string $key = null): ?string + { + return null; + } + + /** + * Prepares the row for saving and returns the storage key for the record. + * + * @param array $row + */ + protected function prepareRow(array &$row): void + { + unset($row[$this->keyField]); + } + + /** + * @param string $key + * @return array + */ + protected function loadRow(string $key): ?array + { + $data = $this->data[$key] ?? []; + if ($this->keyField !== 'storage_key') { + $data[$this->keyField] = $key; + } + $data['__META'] = $this->getObjectMeta($key); + + return $data; + } + + /** + * @param string $key + * @param array $row + * @return array + */ + protected function saveRow(string $key, array $row): array + { + try { + if (isset($row[$this->keyField])) { + $key = $row[$this->keyField]; + } + if (strpos($key, '@@') !== false) { + $key = $this->getNewKey(); + } + + // Check if the row already exists and if the key has been changed. + $oldKey = $row['__META']['storage_key'] ?? null; + if (is_string($oldKey) && $oldKey !== $key) { + $isCopy = $row['__META']['copy'] ?? false; + if ($isCopy) { + $this->copyRow($oldKey, $key); + } else { + $this->renameRow($oldKey, $key); + } + } + + $this->prepareRow($row); + unset($row['__META'], $row['__ERROR']); + + $this->data[$key] = $row; + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf('Flex saveRow(%s): %s', $key, $e->getMessage())); + } + + $row['__META'] = $this->getObjectMeta($key, true); + + return $row; + } + + /** + * @param string $key + * @param bool $variations + * @return array + */ + public function parseKey(string $key, bool $variations = true): array + { + return [ + 'key' => $key, + ]; + } + + protected function save(): void + { + if (null === $this->data) { + $this->buildIndex(); + } + + try { + $path = $this->getStoragePath(); + if (!$path) { + throw new RuntimeException('Storage path is not defined'); + } + $file = $this->getFile($path); + if ($this->prefix) { + $data = new Data((array)$file->content()); + $content = $data->set($this->prefix, $this->data)->toArray(); + } else { + $content = $this->data; + } + $file->save($content); + $this->modified = (int)$file->modified(); // cast false to 0 + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf('Flex save(): %s', $e->getMessage())); + } finally { + if (isset($file)) { + $file->free(); + unset($file); + } + } + } + + /** + * Get key from the filesystem path. + * + * @param string $path + * @return string + */ + protected function getKeyFromPath(string $path): string + { + return basename($path); + } + + /** + * Returns list of all stored keys in [key => timestamp] pairs. + * + * @return array + */ + protected function buildIndex(): array + { + $path = $this->getStoragePath(); + if (!$path) { + $this->data = []; + + return []; + } + + $file = $this->getFile($path); + $this->modified = (int)$file->modified(); // cast false to 0 + + $content = (array) $file->content(); + if ($this->prefix) { + $data = new Data($content); + $content = $data->get($this->prefix, []); + } + + $file->free(); + unset($file); + + $this->data = $content; + + $list = []; + foreach ($this->data as $key => $info) { + $list[$key] = $this->getObjectMeta((string)$key); + } + + return $list; + } + + /** + * @param string $key + * @param bool $reload + * @return array + */ + protected function getObjectMeta(string $key, bool $reload = false): array + { + $modified = isset($this->data[$key]) ? $this->modified : 0; + + return [ + 'storage_key' => $key, + 'key' => $key, + 'storage_timestamp' => $modified + ]; + } + + /** + * @return string + */ + protected function getNewKey(): string + { + if (null === $this->data) { + $this->buildIndex(); + } + + // Make sure that the key doesn't exist. + do { + $key = $this->generateKey(); + } while (isset($this->data[$key])); + + return $key; + } +} diff --git a/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php new file mode 100644 index 0000000..8cbd1d5 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php @@ -0,0 +1,126 @@ +getAuthorizeAction($action); + $scope = $scope ?? $this->getAuthorizeScope(); + + $isMe = null === $user; + if ($isMe) { + $user = $this->getActiveUser(); + } + + if (null === $user) { + return false; + } + + // Finally authorize against given action. + return $this->isAuthorizedOverride($user, $action, $scope, $isMe); + } + + /** + * Please override this method + * + * @param UserInterface $user + * @param string $action + * @param string $scope + * @param bool $isMe + * @return bool|null + */ + protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool + { + return $this->isAuthorizedAction($user, $action, $scope, $isMe); + } + + /** + * Check if user is authorized for the action. + * + * @param UserInterface $user + * @param string $action + * @param string $scope + * @param bool $isMe + * @return bool|null + */ + protected function isAuthorizedAction(UserInterface $user, string $action, string $scope, bool $isMe): ?bool + { + // Check if the action has been denied in the flex type configuration. + $directory = $this instanceof FlexDirectory ? $this : $this->getFlexDirectory(); + $config = $directory->getConfig(); + $allowed = $config->get("{$scope}.actions.{$action}") ?? $config->get("actions.{$action}") ?? true; + if (false === $allowed) { + return false; + } + + // TODO: Not needed anymore with flex users, remove in 2.0. + $auth = $user instanceof FlexObjectInterface ? null : $user->authorize('admin.super'); + if (true === $auth) { + return true; + } + + // Finally authorize the action. + return $user->authorize($this->getAuthorizeRule($scope, $action), !$isMe ? 'test' : null); + } + + /** + * @param UserInterface $user + * @return bool|null + * @deprecated 1.7 Not needed for Flex Users. + */ + protected function isAuthorizedSuperAdmin(UserInterface $user): ?bool + { + // Action authorization includes super user authorization if using Flex Users. + if ($user instanceof FlexObjectInterface) { + return null; + } + + return $user->authorize('admin.super'); + } + + /** + * @param string $scope + * @param string $action + * @return string + */ + protected function getAuthorizeRule(string $scope, string $action): string + { + if ($this instanceof FlexDirectory) { + return $this->getAuthorizeRule($scope, $action); + } + + return $this->getFlexDirectory()->getAuthorizeRule($scope, $action); + } +} diff --git a/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php new file mode 100644 index 0000000..dabfe17 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php @@ -0,0 +1,520 @@ +exists() ? $this->getFlexDirectory()->getStorageFolder($this->getStorageKey()) : null; + } + + /** + * @return string|null + */ + public function getMediaFolder() + { + return $this->exists() ? $this->getFlexDirectory()->getMediaFolder($this->getStorageKey()) : null; + } + + /** + * @return MediaCollectionInterface + */ + public function getMedia() + { + $media = $this->media; + if (null === $media) { + $media = $this->getExistingMedia(); + + // Include uploaded media to the object media. + $this->addUpdatedMedia($media); + } + + return $media; + } + + /** + * @param string $field + * @return MediaCollectionInterface|null + */ + public function getMediaField(string $field): ?MediaCollectionInterface + { + // Field specific media. + $settings = $this->getFieldSettings($field); + if (!empty($settings['media_field'])) { + $var = 'destination'; + } elseif (!empty($settings['media_picker_field'])) { + $var = 'folder'; + } + + if (empty($var)) { + // Not a media field. + $media = null; + } elseif ($settings['self']) { + // Uses main media. + $media = $this->getMedia(); + } else { + // Uses custom media. + $media = new Media($settings[$var]); + $this->addUpdatedMedia($media); + } + + return $media; + } + + /** + * @param string $field + * @return array|null + */ + public function getFieldSettings(string $field): ?array + { + if ($field === '') { + return null; + } + + // Load settings for the field. + $schema = $this->getBlueprint()->schema(); + $settings = $field && is_object($schema) ? (array)$schema->getProperty($field) : null; + if (!is_array($settings)) { + return null; + } + + $type = $settings['type'] ?? ''; + + // Media field. + if (!empty($settings['media_field']) || array_key_exists('destination', $settings) || in_array($type, ['avatar', 'file', 'pagemedia'], true)) { + $settings['media_field'] = true; + $var = 'destination'; + } + + // Media picker field. + if (!empty($settings['media_picker_field']) || in_array($type, ['filepicker', 'pagemediaselect'], true)) { + $settings['media_picker_field'] = true; + $var = 'folder'; + } + + // Set media folder for media fields. + if (isset($var)) { + $folder = $settings[$var] ?? ''; + if (in_array(rtrim($folder, '/'), ['', '@self', 'self@', '@self@'], true)) { + $settings[$var] = $this->getMediaFolder(); + $settings['self'] = true; + } else { + $settings[$var] = Utils::getPathFromToken($folder, $this); + $settings['self'] = false; + } + } + + return $settings; + } + + /** + * @param string $field + * @return array + * @internal + */ + protected function getMediaFieldSettings(string $field): array + { + $settings = $this->getFieldSettings($field) ?? []; + + return $settings + ['accept' => '*', 'limit' => 1000, 'self' => true]; + } + + protected function getMediaFields(): array + { + // Load settings for the field. + $schema = $this->getBlueprint()->schema(); + + $list = []; + foreach ($schema->getState()['items'] as $field => $settings) { + if (isset($settings['type']) && (in_array($settings['type'], ['avatar', 'file', 'pagemedia']) || !empty($settings['destination']))) { + $list[] = $field; + } + } + + return $list; + } + + /** + * @param array|mixed $value + * @param array $settings + * @return array|mixed + */ + protected function parseFileProperty($value, array $settings = []) + { + if (!is_array($value)) { + return $value; + } + + $media = $this->getMedia(); + $originalMedia = is_callable([$this, 'getOriginalMedia']) ? $this->getOriginalMedia() : null; + + $list = []; + foreach ($value as $filename => $info) { + if (!is_array($info)) { + $list[$filename] = $info; + continue; + } + + if (is_int($filename)) { + $filename = $info['path'] ?? $info['name']; + } + + /** @var Medium|null $imageFile */ + $imageFile = $media[$filename]; + + /** @var Medium|null $originalFile */ + $originalFile = $originalMedia ? $originalMedia[$filename] : null; + + $url = $imageFile ? $imageFile->url() : null; + $originalUrl = $originalFile ? $originalFile->url() : null; + $list[$filename] = [ + 'name' => $info['name'] ?? null, + 'type' => $info['type'] ?? null, + 'size' => $info['size'] ?? null, + 'path' => $filename, + 'thumb_url' => $url, + 'image_url' => $originalUrl ?? $url + ]; + if ($originalFile) { + $list[$filename]['cropData'] = (object)($originalFile->metadata()['upload']['crop'] ?? []); + } + } + + return $list; + } + + /** + * @param UploadedFileInterface $uploadedFile + * @param string|null $filename + * @param string|null $field + * @return void + * @internal + */ + public function checkUploadedMediaFile(UploadedFileInterface $uploadedFile, string $filename = null, string $field = null) + { + $media = $this->getMedia(); + if (!$media instanceof MediaUploadInterface) { + throw new RuntimeException("Media for {$this->getFlexDirectory()->getFlexType()} doesn't support file uploads."); + } + + $media->checkUploadedFile($uploadedFile, $filename, $this->getMediaFieldSettings($field ?? '')); + } + + /** + * @param UploadedFileInterface $uploadedFile + * @param string|null $filename + * @param string|null $field + * @return void + * @internal + */ + public function uploadMediaFile(UploadedFileInterface $uploadedFile, string $filename = null, string $field = null): void + { + $settings = $this->getMediaFieldSettings($field ?? ''); + + $media = $field ? $this->getMediaField($field) : $this->getMedia(); + if (!$media instanceof MediaUploadInterface) { + throw new RuntimeException("Media for {$this->getFlexDirectory()->getFlexType()} doesn't support file uploads."); + } + + $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings); + $media->copyUploadedFile($uploadedFile, $filename, $settings); + $this->clearMediaCache(); + } + + /** + * @param string $filename + * @return void + * @internal + */ + public function deleteMediaFile(string $filename): void + { + $media = $this->getMedia(); + if (!$media instanceof MediaUploadInterface) { + throw new RuntimeException("Media for {$this->getFlexDirectory()->getFlexType()} doesn't support file uploads."); + } + + $media->deleteFile($filename); + $this->clearMediaCache(); + } + + /** + * @return array + */ + public function __debugInfo() + { + return parent::__debugInfo() + [ + 'uploads:private' => $this->getUpdatedMedia() + ]; + } + + /** + * @param array $files + * @return void + */ + protected function setUpdatedMedia(array $files): void + { + $media = $this->getMedia(); + if (!$media instanceof MediaUploadInterface) { + return; + } + + $filesystem = Filesystem::getInstance(false); + + $list = []; + foreach ($files as $field => $group) { + $field = (string)$field; + // Ignore files without a field and resized images. + if ($field === '' || strpos($field, '/')) { + continue; + } + + // Load settings for the field. + $settings = $this->getMediaFieldSettings($field); + foreach ($group as $filename => $file) { + if ($file) { + // File upload. + $filename = $file->getClientFilename(); + + /** @var FormFlashFile $file */ + $data = $file->jsonSerialize(); + unset($data['tmp_name'], $data['path']); + } else { + // File delete. + $data = null; + } + + if ($file) { + // Check file upload against media limits (except for max size). + $filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings); + } + + $self = $settings['self']; + if ($this->_loadMedia && $self) { + $filepath = $filename; + } else { + $filepath = "{$settings['destination']}/{$filename}"; + } + + // Calculate path without the retina scaling factor. + $realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath)); + + $list[$filename] = [$file, $settings]; + + $path = str_replace('.', "\n", $field); + if (null !== $data) { + $data['name'] = $filename; + $data['path'] = $filepath; + + $this->setNestedProperty("{$path}\n{$realpath}", $data, "\n"); + } else { + $this->unsetNestedProperty("{$path}\n{$realpath}", "\n"); + } + } + } + + $this->clearMediaCache(); + + $this->_uploads = $list; + } + + /** + * @param MediaCollectionInterface $media + */ + protected function addUpdatedMedia(MediaCollectionInterface $media): void + { + $updated = false; + foreach ($this->getUpdatedMedia() as $filename => $upload) { + if (is_array($upload)) { + // Uses new format with [UploadedFileInterface, array]. + $settings = $upload[1]; + if (isset($settings['destination']) && $settings['destination'] === $media->getPath()) { + $upload = $upload[0]; + } else { + $upload = false; + } + } + if (false !== $upload) { + $medium = $upload ? MediumFactory::fromUploadedFile($upload) : null; + $updated = true; + if ($medium) { + $medium->uploaded = true; + $media->add($filename, $medium); + } elseif (is_callable([$media, 'hide'])) { + $media->hide($filename); + } + } + } + + if ($updated) { + $media->setTimestamps(); + } + } + + /** + * @return array + */ + protected function getUpdatedMedia(): array + { + return $this->_uploads ?? []; + } + + /** + * @return void + */ + protected function saveUpdatedMedia(): void + { + $media = $this->getMedia(); + if (!$media instanceof MediaUploadInterface) { + return; + } + + // Upload/delete altered files. + /** + * @var string $filename + * @var UploadedFileInterface|array|null $file + */ + foreach ($this->getUpdatedMedia() as $filename => $file) { + if (is_array($file)) { + [$file, $settings] = $file; + } else { + $settings = null; + } + if ($file instanceof UploadedFileInterface) { + $media->copyUploadedFile($file, $filename, $settings); + } else { + $media->deleteFile($filename, $settings); + } + } + + $this->setUpdatedMedia([]); + $this->clearMediaCache(); + } + + /** + * @return void + */ + protected function freeMedia(): void + { + $this->unsetObjectProperty('media'); + } + + /** + * @param string $uri + * @return Medium|null + */ + protected function createMedium($uri) + { + $grav = Grav::instance(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + $file = $uri && $locator->isStream($uri) ? $locator->findResource($uri) : $uri; + + return is_string($file) && file_exists($file) ? MediumFactory::fromFile($file) : null; + } + + /** + * @return CacheInterface + */ + protected function getMediaCache() + { + return $this->getCache('object'); + } + + /** + * @return MediaCollectionInterface + */ + protected function offsetLoad_media() + { + return $this->getMedia(); + } + + /** + * @return null + */ + protected function offsetSerialize_media() + { + return null; + } + + /** + * @return FlexDirectory + */ + abstract public function getFlexDirectory(): FlexDirectory; + + /** + * @return string + */ + abstract public function getStorageKey(): string; + + /** + * @param string $filename + * @return void + * @deprecated 1.7 Use Media class that implements MediaUploadInterface instead. + */ + public function checkMediaFilename(string $filename) + { + user_error(__METHOD__ . '() is deprecated since Grav 1.7, use Media class that implements MediaUploadInterface instead', E_USER_DEPRECATED); + + // Check the file extension. + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + + $grav = Grav::instance(); + + /** @var Config $config */ + $config = $grav['config']; + + // If not a supported type, return + if (!$extension || !$config->get("media.types.{$extension}")) { + $language = $grav['language']; + throw new RuntimeException($language->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400); + } + } +} diff --git a/system/src/Grav/Framework/Flex/Traits/FlexRelatedDirectoryTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexRelatedDirectoryTrait.php new file mode 100644 index 0000000..9f7380c --- /dev/null +++ b/system/src/Grav/Framework/Flex/Traits/FlexRelatedDirectoryTrait.php @@ -0,0 +1,59 @@ +getRelatedDirectory($type); + $collection = $directory->getCollection(); + $list = $this->getNestedProperty($property) ?: []; + + /** @var FlexCollection $collection */ + $collection = $collection->filter(static function ($object) use ($list) { + return in_array($object->id, $list, true); + }); + + return $collection; + } + + /** + * @param string $type + * @return FlexDirectory + * @throws RuntimeException + */ + protected function getRelatedDirectory($type): FlexDirectory + { + $directory = $this->getFlexContainer()->getDirectory($type); + if (!$directory) { + throw new RuntimeException(ucfirst($type). ' directory does not exist!'); + } + + return $directory; + } +} diff --git a/system/src/Grav/Framework/Form/FormFlash.php b/system/src/Grav/Framework/Form/FormFlash.php new file mode 100644 index 0000000..864e992 --- /dev/null +++ b/system/src/Grav/Framework/Form/FormFlash.php @@ -0,0 +1,572 @@ + $args[0], + 'unique_id' => $args[1] ?? null, + 'form_name' => $args[2] ?? null, + ]; + $config = array_filter($config, static function ($val) { + return $val !== null; + }); + } + + $this->sessionId = $config['session_id'] ?? 'no-session'; + $this->uniqueId = $config['unique_id'] ?? ''; + + $folder = $config['folder'] ?? ($this->sessionId ? 'tmp://forms/' . $this->sessionId : ''); + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + $this->folder = $folder && $locator->isStream($folder) ? $locator->findResource($folder, true, true) : $folder; + + $this->init($this->loadStoredForm(), $config); + } + + /** + * @param array|null $data + * @param array $config + */ + protected function init(?array $data, array $config): void + { + if (null === $data) { + $this->exists = false; + $this->formName = $config['form_name'] ?? ''; + $this->url = ''; + $this->createdTimestamp = $this->updatedTimestamp = time(); + $this->files = []; + } else { + $this->exists = true; + $this->formName = $data['form'] ?? $config['form_name'] ?? ''; + $this->url = $data['url'] ?? ''; + $this->user = $data['user'] ?? null; + $this->updatedTimestamp = $data['timestamps']['updated'] ?? time(); + $this->createdTimestamp = $data['timestamps']['created'] ?? $this->updatedTimestamp; + $this->data = $data['data'] ?? null; + $this->files = $data['files'] ?? []; + } + } + + /** + * Load raw flex flash data from the filesystem. + * + * @return array|null + */ + protected function loadStoredForm(): ?array + { + $file = $this->getTmpIndex(); + $exists = $file && $file->exists(); + + $data = null; + if ($exists) { + try { + $data = (array)$file->content(); + } catch (Exception $e) { + } + } + + return $data; + } + + /** + * @inheritDoc + */ + public function getSessionId(): string + { + return $this->sessionId; + } + + /** + * @inheritDoc + */ + public function getUniqueId(): string + { + return $this->uniqueId; + } + + /** + * @return string + * @deprecated 1.6.11 Use '->getUniqueId()' method instead. + */ + public function getUniqieId(): string + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6.11, use ->getUniqueId() method instead', E_USER_DEPRECATED); + + return $this->getUniqueId(); + } + + /** + * @inheritDoc + */ + public function getFormName(): string + { + return $this->formName; + } + + + /** + * @inheritDoc + */ + public function getUrl(): string + { + return $this->url; + } + + /** + * @inheritDoc + */ + public function getUsername(): string + { + return $this->user['username'] ?? ''; + } + + /** + * @inheritDoc + */ + public function getUserEmail(): string + { + return $this->user['email'] ?? ''; + } + + /** + * @inheritDoc + */ + public function getCreatedTimestamp(): int + { + return $this->createdTimestamp; + } + + /** + * @inheritDoc + */ + public function getUpdatedTimestamp(): int + { + return $this->updatedTimestamp; + } + + + /** + * @inheritDoc + */ + public function getData(): ?array + { + return $this->data; + } + + /** + * @inheritDoc + */ + public function setData(?array $data): void + { + $this->data = $data; + } + + /** + * @inheritDoc + */ + public function exists(): bool + { + return $this->exists; + } + + /** + * @inheritDoc + */ + public function save(bool $force = false) + { + if (!($this->folder && $this->uniqueId)) { + return $this; + } + + if ($force || $this->data || $this->files) { + // Only save if there is data or files to be saved. + $file = $this->getTmpIndex(); + if ($file) { + $file->save($this->jsonSerialize()); + $this->exists = true; + } + } elseif ($this->exists) { + // Delete empty form flash if it exists (it carries no information). + return $this->delete(); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function delete() + { + if ($this->folder && $this->uniqueId) { + $this->removeTmpDir(); + $this->files = []; + $this->exists = false; + } + + return $this; + } + + /** + * @inheritDoc + */ + public function getFilesByField(string $field): array + { + if (!isset($this->uploadObjects[$field])) { + $objects = []; + foreach ($this->files[$field] ?? [] as $name => $upload) { + $objects[$name] = $upload ? new FormFlashFile($field, $upload, $this) : null; + } + $this->uploadedFiles[$field] = $objects; + } + + return $this->uploadedFiles[$field]; + } + + /** + * @inheritDoc + */ + public function getFilesByFields($includeOriginal = false): array + { + $list = []; + foreach ($this->files as $field => $values) { + if (!$includeOriginal && strpos($field, '/')) { + continue; + } + $list[$field] = $this->getFilesByField($field); + } + + return $list; + } + + /** + * @inheritDoc + */ + public function addUploadedFile(UploadedFileInterface $upload, string $field = null, array $crop = null): string + { + $tmp_dir = $this->getTmpDir(); + $tmp_name = Utils::generateRandomString(12); + $name = $upload->getClientFilename(); + if (!$name) { + throw new RuntimeException('Uploaded file has no filename'); + } + + // Prepare upload data for later save + $data = [ + 'name' => $name, + 'type' => $upload->getClientMediaType(), + 'size' => $upload->getSize(), + 'tmp_name' => $tmp_name + ]; + + Folder::create($tmp_dir); + $upload->moveTo("{$tmp_dir}/{$tmp_name}"); + + $this->addFileInternal($field, $name, $data, $crop); + + return $name; + } + + /** + * @inheritDoc + */ + public function addFile(string $filename, string $field, array $crop = null): bool + { + if (!file_exists($filename)) { + throw new RuntimeException("File not found: {$filename}"); + } + + // Prepare upload data for later save + $data = [ + 'name' => basename($filename), + 'type' => Utils::getMimeByLocalFile($filename), + 'size' => filesize($filename), + ]; + + $this->addFileInternal($field, $data['name'], $data, $crop); + + return true; + } + + /** + * @inheritDoc + */ + public function removeFile(string $name, string $field = null): bool + { + if (!$name) { + return false; + } + + $field = $field ?: 'undefined'; + + $upload = $this->files[$field][$name] ?? null; + if (null !== $upload) { + $this->removeTmpFile($upload['tmp_name'] ?? ''); + } + $upload = $this->files[$field . '/original'][$name] ?? null; + if (null !== $upload) { + $this->removeTmpFile($upload['tmp_name'] ?? ''); + } + + // Mark file as deleted. + $this->files[$field][$name] = null; + $this->files[$field . '/original'][$name] = null; + + unset( + $this->uploadedFiles[$field][$name], + $this->uploadedFiles[$field . '/original'][$name] + ); + + return true; + } + + /** + * @inheritDoc + */ + public function clearFiles() + { + foreach ($this->files as $field => $files) { + foreach ($files as $name => $upload) { + $this->removeTmpFile($upload['tmp_name'] ?? ''); + } + } + + $this->files = []; + } + + /** + * @inheritDoc + */ + public function jsonSerialize(): array + { + return [ + 'form' => $this->formName, + 'unique_id' => $this->uniqueId, + 'url' => $this->url, + 'user' => $this->user, + 'timestamps' => [ + 'created' => $this->createdTimestamp, + 'updated' => time(), + ], + 'data' => $this->data, + 'files' => $this->files + ]; + } + + /** + * @param string $url + * @return $this + */ + public function setUrl(string $url): self + { + $this->url = $url; + + return $this; + } + + /** + * @param UserInterface|null $user + * @return $this + */ + public function setUser(UserInterface $user = null) + { + if ($user && $user->username) { + $this->user = [ + 'username' => $user->username, + 'email' => $user->email ?? '' + ]; + } else { + $this->user = null; + } + + return $this; + } + + /** + * @param string|null $username + * @return $this + */ + public function setUserName(string $username = null): self + { + $this->user['username'] = $username; + + return $this; + } + + /** + * @param string|null $email + * @return $this + */ + public function setUserEmail(string $email = null): self + { + $this->user['email'] = $email; + + return $this; + } + + /** + * @return string + */ + public function getTmpDir(): string + { + return $this->folder && $this->uniqueId ? "{$this->folder}/{$this->uniqueId}" : ''; + } + + /** + * @return ?YamlFile + */ + protected function getTmpIndex(): ?YamlFile + { + $tmpDir = $this->getTmpDir(); + + // Do not use CompiledYamlFile as the file can change multiple times per second. + return $tmpDir ? YamlFile::instance($tmpDir . '/index.yaml') : null; + } + + /** + * @param string $name + */ + protected function removeTmpFile(string $name): void + { + $tmpDir = $this->getTmpDir(); + $filename = $tmpDir ? $tmpDir . '/' . $name : ''; + if ($name && $filename && is_file($filename)) { + unlink($filename); + } + } + + /** + * @return void + */ + protected function removeTmpDir(): void + { + // Make sure that index file cache gets always cleared. + $file = $this->getTmpIndex(); + if ($file) { + $file->free(); + } + + $tmpDir = $this->getTmpDir(); + if ($tmpDir && file_exists($tmpDir)) { + Folder::delete($tmpDir); + } + } + + /** + * @param string|null $field + * @param string $name + * @param array $data + * @param array|null $crop + * @return void + */ + protected function addFileInternal(?string $field, string $name, array $data, array $crop = null): void + { + if (!($this->folder && $this->uniqueId)) { + throw new RuntimeException('Cannot upload files: form flash folder not defined'); + } + + $field = $field ?: 'undefined'; + if (!isset($this->files[$field])) { + $this->files[$field] = []; + } + + $oldUpload = $this->files[$field][$name] ?? null; + + if ($crop) { + // Deal with crop upload + if ($oldUpload) { + $originalUpload = $this->files[$field . '/original'][$name] ?? null; + if ($originalUpload) { + // If there is original file already present, remove the modified file + $this->files[$field . '/original'][$name]['crop'] = $crop; + $this->removeTmpFile($oldUpload['tmp_name'] ?? ''); + } else { + // Otherwise make the previous file as original + $oldUpload['crop'] = $crop; + $this->files[$field . '/original'][$name] = $oldUpload; + } + } else { + $this->files[$field . '/original'][$name] = [ + 'name' => $name, + 'type' => $data['type'], + 'crop' => $crop + ]; + } + } else { + // Deal with replacing upload + $originalUpload = $this->files[$field . '/original'][$name] ?? null; + $this->files[$field . '/original'][$name] = null; + + $this->removeTmpFile($oldUpload['tmp_name'] ?? ''); + $this->removeTmpFile($originalUpload['tmp_name'] ?? ''); + } + + // Prepare data to be saved later + $this->files[$field][$name] = $data; + } +} diff --git a/system/src/Grav/Framework/Form/FormFlashFile.php b/system/src/Grav/Framework/Form/FormFlashFile.php new file mode 100644 index 0000000..dc510e2 --- /dev/null +++ b/system/src/Grav/Framework/Form/FormFlashFile.php @@ -0,0 +1,237 @@ +field = $field; + $this->upload = $upload; + $this->flash = $flash; + + $tmpFile = $this->getTmpFile(); + if (!$tmpFile && $this->isOk()) { + $this->upload['error'] = \UPLOAD_ERR_NO_FILE; + } + + if (!isset($this->upload['size'])) { + $this->upload['size'] = $tmpFile && $this->isOk() ? filesize($tmpFile) : 0; + } + } + + /** + * @return StreamInterface + */ + public function getStream() + { + $this->validateActive(); + + $tmpFile = $this->getTmpFile(); + if (null === $tmpFile) { + throw new RuntimeException('No temporary file'); + } + + $resource = fopen($tmpFile, 'rb'); + if (false === $resource) { + throw new RuntimeException('No temporary file'); + } + + return Stream::create($resource); + } + + /** + * @param string $targetPath + * @return void + */ + public function moveTo($targetPath) + { + $this->validateActive(); + + if (!is_string($targetPath) || empty($targetPath)) { + throw new InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string'); + } + $tmpFile = $this->getTmpFile(); + if (null === $tmpFile) { + throw new RuntimeException('No temporary file'); + } + + $this->moved = copy($tmpFile, $targetPath); + + if (false === $this->moved) { + throw new RuntimeException(sprintf('Uploaded file could not be moved to %s', $targetPath)); + } + + $filename = $this->getClientFilename(); + if ($filename) { + $this->flash->removeFile($filename, $this->field); + } + } + + /** + * @return string + */ + public function getField(): string + { + return $this->field; + } + + /** + * @return int + */ + public function getSize() + { + return $this->upload['size']; + } + + /** + * @return int + */ + public function getError() + { + return $this->upload['error'] ?? \UPLOAD_ERR_OK; + } + + /** + * @return string + */ + public function getClientFilename() + { + return $this->upload['name'] ?? 'unknown'; + } + + /** + * @return string + */ + public function getClientMediaType() + { + return $this->upload['type'] ?? 'application/octet-stream'; + } + + /** + * @return bool + */ + public function isMoved(): bool + { + return $this->moved; + } + + /** + * @return array + */ + public function getMetaData(): array + { + if (isset($this->upload['crop'])) { + return ['crop' => $this->upload['crop']]; + } + + return []; + } + + /** + * @return string + */ + public function getDestination() + { + return $this->upload['path'] ?? ''; + } + + /** + * @return array + */ + public function jsonSerialize() + { + return $this->upload; + } + + /** + * @return string|null + */ + public function getTmpFile(): ?string + { + $tmpName = $this->upload['tmp_name'] ?? null; + + if (!$tmpName) { + return null; + } + + $tmpFile = $this->flash->getTmpDir() . '/' . $tmpName; + + return file_exists($tmpFile) ? $tmpFile : null; + } + + /** + * @return array + */ + public function __debugInfo() + { + return [ + 'field:private' => $this->field, + 'moved:private' => $this->moved, + 'upload:private' => $this->upload, + ]; + } + + /** + * @return void + * @throws RuntimeException if is moved or not ok + */ + private function validateActive(): void + { + if (!$this->isOk()) { + throw new RuntimeException('Cannot retrieve stream due to upload error'); + } + + if ($this->moved) { + throw new RuntimeException('Cannot retrieve stream after it has already been moved'); + } + + if (!$this->getTmpFile()) { + throw new RuntimeException('Cannot retrieve stream as the file is missing'); + } + } + + /** + * @return bool return true if there is no upload error + */ + private function isOk(): bool + { + return \UPLOAD_ERR_OK === $this->getError(); + } +} diff --git a/system/src/Grav/Framework/Form/Interfaces/FormFactoryInterface.php b/system/src/Grav/Framework/Form/Interfaces/FormFactoryInterface.php new file mode 100644 index 0000000..d21d4aa --- /dev/null +++ b/system/src/Grav/Framework/Form/Interfaces/FormFactoryInterface.php @@ -0,0 +1,42 @@ +id; + } + + /** + * @param string $id + */ + public function setId(string $id): void + { + $this->id = $id; + } + + /** + * @return void + */ + public function disable(): void + { + $this->enabled = false; + } + + /** + * @return void + */ + public function enable(): void + { + $this->enabled = true; + } + + /** + * @return bool + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * @return string + */ + public function getUniqueId(): string + { + return $this->uniqueid; + } + + /** + * @param string $uniqueId + * @return void + */ + public function setUniqueId(string $uniqueId): void + { + $this->uniqueid = $uniqueId; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getFormName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getNonceName(): string + { + return 'form-nonce'; + } + + /** + * @return string + */ + public function getNonceAction(): string + { + return 'form'; + } + + /** + * @return string + */ + public function getNonce(): string + { + return Utils::getNonce($this->getNonceAction()); + } + + /** + * @return string + */ + public function getAction(): string + { + return ''; + } + + /** + * @return string + */ + public function getTask(): string + { + return $this->getBlueprint()->get('form/task') ?? ''; + } + + /** + * @param string|null $name + * @return mixed + */ + public function getData(string $name = null) + { + return null !== $name ? $this->data[$name] : $this->data; + } + + /** + * @return array|UploadedFileInterface[] + */ + public function getFiles(): array + { + return $this->files ?? []; + } + + /** + * @param string $name + * @return mixed|null + */ + public function getValue(string $name) + { + return $this->data[$name] ?? null; + } + + /** + * @param string $name + * @return mixed|null + */ + public function getDefaultValue(string $name) + { + $path = explode('.', $name) ?: []; + $offset = array_shift($path) ?? ''; + + $current = $this->getDefaultValues(); + + if (!isset($current[$offset])) { + return null; + } + + $current = $current[$offset]; + + while ($path) { + $offset = array_shift($path); + + if ((is_array($current) || $current instanceof ArrayAccess) && isset($current[$offset])) { + $current = $current[$offset]; + } elseif (is_object($current) && isset($current->{$offset})) { + $current = $current->{$offset}; + } else { + return null; + } + }; + + return $current; + } + + /** + * @return array + */ + public function getDefaultValues(): array + { + return $this->getBlueprint()->getDefaults(); + } + + /** + * @param ServerRequestInterface $request + * @return FormInterface|$this + */ + public function handleRequest(ServerRequestInterface $request): FormInterface + { + // Set current form to be active. + $grav = Grav::instance(); + $forms = $grav['forms'] ?? null; + if ($forms) { + $forms->setActiveForm($this); + + /** @var Twig $twig */ + $twig = $grav['twig']; + $twig->twig_vars['form'] = $this; + } + + try { + [$data, $files] = $this->parseRequest($request); + + $this->submit($data, $files); + } catch (Exception $e) { + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addException($e); + + $this->setError($e->getMessage()); + } + + return $this; + } + + /** + * @param ServerRequestInterface $request + * @return FormInterface|$this + */ + public function setRequest(ServerRequestInterface $request): FormInterface + { + [$data, $files] = $this->parseRequest($request); + + $this->data = new Data($data, $this->getBlueprint()); + $this->files = $files; + + return $this; + } + + /** + * @return bool + */ + public function isValid(): bool + { + return $this->status === 'success'; + } + + /** + * @return string|null + */ + public function getError(): ?string + { + return !$this->isValid() ? $this->message : null; + } + + /** + * @return array + */ + public function getErrors(): array + { + return !$this->isValid() ? $this->messages : []; + } + + /** + * @return bool + */ + public function isSubmitted(): bool + { + return $this->submitted; + } + + /** + * @return bool + */ + public function validate(): bool + { + if (!$this->isValid()) { + return false; + } + + try { + $this->validateData($this->data); + $this->validateUploads($this->getFiles()); + } catch (ValidationException $e) { + $this->setErrors($e->getMessages()); + } catch (Exception $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + $this->setError($e->getMessage()); + } + + $this->filterData($this->data); + + return $this->isValid(); + } + + /** + * @param array $data + * @param UploadedFileInterface[]|null $files + * @return FormInterface|$this + */ + public function submit(array $data, array $files = null): FormInterface + { + try { + if ($this->isSubmitted()) { + throw new RuntimeException('Form has already been submitted'); + } + + $this->data = new Data($data, $this->getBlueprint()); + $this->files = $files ?? []; + + if (!$this->validate()) { + return $this; + } + + $this->doSubmit($this->data->toArray(), $this->files); + + $this->submitted = true; + } catch (Exception $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + $this->setError($e->getMessage()); + } + + return $this; + } + + /** + * @return void + */ + public function reset(): void + { + // Make sure that the flash object gets deleted. + $this->getFlash()->delete(); + + $this->data = null; + $this->files = []; + $this->status = 'success'; + $this->message = null; + $this->messages = []; + $this->submitted = false; + $this->flash = null; + } + + /** + * @return array + */ + public function getFields(): array + { + return $this->getBlueprint()->fields(); + } + + /** + * @return array + */ + public function getButtons(): array + { + return $this->getBlueprint()->get('form/buttons') ?? []; + } + + /** + * @return array + */ + public function getTasks(): array + { + return $this->getBlueprint()->get('form/tasks') ?? []; + } + + /** + * @return Blueprint + */ + abstract public function getBlueprint(): Blueprint; + + /** + * Get form flash object. + * + * @return FormFlashInterface + */ + public function getFlash() + { + if (null === $this->flash) { + $grav = Grav::instance(); + $config = [ + 'session_id' => $this->getSessionId(), + 'unique_id' => $this->getUniqueId(), + 'form_name' => $this->getName(), + 'folder' => $this->getFlashFolder() + ]; + + + $this->flash = new FormFlash($config); + $this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null); + } + + return $this->flash; + } + + /** + * Get all available form flash objects for this form. + * + * @return FormFlashInterface[] + */ + public function getAllFlashes(): array + { + $folder = $this->getFlashFolder(); + if (!$folder || !is_dir($folder)) { + return []; + } + + $name = $this->getName(); + + $list = []; + /** @var SplFileInfo $file */ + foreach (new FilesystemIterator($folder) as $file) { + $uniqueId = $file->getFilename(); + $config = [ + 'session_id' => $this->getSessionId(), + 'unique_id' => $uniqueId, + 'form_name' => $name, + 'folder' => $this->getFlashFolder() + ]; + $flash = new FormFlash($config); + if ($flash->exists() && $flash->getFormName() === $name) { + $list[] = $flash; + } + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FormInterface::render() + */ + public function render(string $layout = null, array $context = []) + { + if (null === $layout) { + $layout = 'default'; + } + + $grav = Grav::instance(); + + $block = HtmlBlock::create(); + $block->disableCache(); + + $output = $this->getTemplate($layout)->render( + ['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'form' => $this, 'layout' => $layout] + $context + ); + + $block->setContent($output); + + return $block; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->doSerialize(); + } + + /** + * @return array + */ + final public function __serialize(): array + { + return $this->doSerialize(); + } + + /** + * @param array $data + * @return void + */ + final public function __unserialize(array $data): void + { + $this->doUnserialize($data); + } + + protected function getSessionId(): string + { + if (null === $this->sessionid) { + /** @var Grav $grav */ + $grav = Grav::instance(); + + /** @var SessionInterface|null $session */ + $session = $grav['session'] ?? null; + + $this->sessionid = $session ? ($session->getId() ?? '') : ''; + } + + return $this->sessionid; + } + + /** + * @param string $sessionId + * @return void + */ + protected function setSessionId(string $sessionId): void + { + $this->sessionid = $sessionId; + } + + /** + * @return void + */ + protected function unsetFlash(): void + { + $this->flash = null; + } + + /** + * @return string|null + */ + protected function getFlashFolder(): ?string + { + $grav = Grav::instance(); + + /** @var UserInterface|null $user */ + $user = $grav['user'] ?? null; + if (null !== $user && $user->exists()) { + $username = $user->username; + $mediaFolder = $user->getMediaFolder(); + } else { + $username = null; + $mediaFolder = null; + } + $session = $grav['session'] ?? null; + $sessionId = $session ? $session->getId() : null; + + // Fill template token keys/value pairs. + $dataMap = [ + '[FORM_NAME]' => $this->getName(), + '[SESSIONID]' => $sessionId ?? '!!', + '[USERNAME]' => $username ?? '!!', + '[USERNAME_OR_SESSIONID]' => $username ?? $sessionId ?? '!!', + '[ACCOUNT]' => $mediaFolder ?? '!!' + ]; + + $flashLookupFolder = $this->getFlashLookupFolder(); + + $path = str_replace(array_keys($dataMap), array_values($dataMap), $flashLookupFolder); + + // Make sure we only return valid paths. + return strpos($path, '!!') === false ? rtrim($path, '/') : null; + } + + /** + * @return string + */ + protected function getFlashLookupFolder(): string + { + if (null === $this->flashFolder) { + $this->flashFolder = $this->getBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]'; + } + + return $this->flashFolder; + } + + /** + * @param string $folder + * @return void + */ + protected function setFlashLookupFolder(string $folder): void + { + $this->flashFolder = $folder; + } + + /** + * Set a single error. + * + * @param string $error + * @return void + */ + protected function setError(string $error): void + { + $this->status = 'error'; + $this->message = $error; + } + + /** + * Set all errors. + * + * @param array $errors + * @return void + */ + protected function setErrors(array $errors): void + { + $this->status = 'error'; + $this->messages = $errors; + } + + /** + * @param string $layout + * @return Template|TemplateWrapper + * @throws LoaderError + * @throws SyntaxError + */ + protected function getTemplate($layout) + { + $grav = Grav::instance(); + + /** @var Twig $twig */ + $twig = $grav['twig']; + + return $twig->twig()->resolveTemplate( + [ + "forms/{$layout}/form.html.twig", + 'forms/default/form.html.twig' + ] + ); + } + + /** + * Parse PSR-7 ServerRequest into data and files. + * + * @param ServerRequestInterface $request + * @return array + */ + protected function parseRequest(ServerRequestInterface $request): array + { + $method = $request->getMethod(); + if (!in_array($method, ['PUT', 'POST', 'PATCH'])) { + throw new RuntimeException(sprintf('FlexForm: Bad HTTP method %s', $method)); + } + + $body = $request->getParsedBody(); + $data = isset($body['data']) ? $this->decodeData($body['data']) : null; + + $flash = $this->getFlash(); + /* + if (null !== $data) { + $flash->setData($data); + $flash->save(); + } + */ + + $blueprint = $this->getBlueprint(); + $includeOriginal = (bool)($blueprint->form()['images']['original'] ?? null); + $files = $flash->getFilesByFields($includeOriginal); + + $data = $blueprint->processForm($data ?? [], $body['toggleable_data'] ?? []); + + return [ + $data, + $files + ]; + } + + /** + * Validate data and throw validation exceptions if validation fails. + * + * @param ArrayAccess|Data|null $data + * @return void + * @throws ValidationException + * @throws Exception + */ + protected function validateData($data = null): void + { + if ($data instanceof Data) { + $data->validate(); + } + } + + /** + * Filter validated data. + * + * @param ArrayAccess|Data|null $data + * @return void + */ + protected function filterData($data = null): void + { + if ($data instanceof Data) { + $data->filter(); + } + } + + /** + * Validate all uploaded files. + * + * @param array $files + * @return void + */ + protected function validateUploads(array $files): void + { + foreach ($files as $file) { + if (null === $file) { + continue; + } + if ($file instanceof UploadedFileInterface) { + $this->validateUpload($file); + } else { + $this->validateUploads($file); + } + } + } + + /** + * Validate uploaded file. + * + * @param UploadedFileInterface $file + * @return void + */ + protected function validateUpload(UploadedFileInterface $file): void + { + // Handle bad filenames. + $filename = $file->getClientFilename(); + + if ($filename && !Utils::checkFilename($filename)) { + $grav = Grav::instance(); + throw new RuntimeException( + sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_UPLOAD', null, true), $filename, 'Bad filename') + ); + } + } + + /** + * Decode POST data + * + * @param array $data + * @return array + */ + protected function decodeData($data): array + { + if (!is_array($data)) { + return []; + } + + // Decode JSON encoded fields and merge them to data. + if (isset($data['_json'])) { + $data = array_replace_recursive($data, $this->jsonDecode($data['_json'])); + if (null === $data) { + throw new RuntimeException(__METHOD__ . '(): Unexpected error'); + } + unset($data['_json']); + } + + return $data; + } + + /** + * Recursively JSON decode POST data. + * + * @param array $data + * @return array + */ + protected function jsonDecode(array $data): array + { + foreach ($data as $key => &$value) { + if (is_array($value)) { + $value = $this->jsonDecode($value); + } elseif (trim($value) === '') { + unset($data[$key]); + } else { + $value = json_decode($value, true); + if ($value === null && json_last_error() !== JSON_ERROR_NONE) { + unset($data[$key]); + $this->setError("Badly encoded JSON data (for {$key}) was sent to the form"); + } + } + } + + return $data; + } + + /** + * @return array + */ + protected function doSerialize(): array + { + $data = $this->data instanceof Data ? $this->data->toArray() : null; + + return [ + 'name' => $this->name, + 'id' => $this->id, + 'uniqueid' => $this->uniqueid, + 'submitted' => $this->submitted, + 'status' => $this->status, + 'message' => $this->message, + 'messages' => $this->messages, + 'data' => $data, + 'files' => $this->files, + ]; + } + + /** + * @param array $data + * @return void + */ + protected function doUnserialize(array $data): void + { + $this->name = $data['name']; + $this->id = $data['id']; + $this->uniqueid = $data['uniqueid']; + $this->submitted = $data['submitted'] ?? false; + $this->status = $data['status'] ?? 'success'; + $this->message = $data['message'] ?? null; + $this->messages = $data['messages'] ?? []; + $this->data = isset($data['data']) ? new Data($data['data'], $this->getBlueprint()) : null; + $this->files = $data['files'] ?? []; + } +} diff --git a/system/src/Grav/Framework/Interfaces/RenderInterface.php b/system/src/Grav/Framework/Interfaces/RenderInterface.php new file mode 100644 index 0000000..7a7d9d3 --- /dev/null +++ b/system/src/Grav/Framework/Interfaces/RenderInterface.php @@ -0,0 +1,38 @@ +render('custom', ['variable' => 'value']); + * @example {% render object layout 'custom' with { variable: 'value' } %} + * + * @param string|null $layout Layout to be used. + * @param array $context Extra context given to the renderer. + * + * @return ContentBlockInterface|HtmlBlock Returns `HtmlBlock` containing the rendered output. + * @api + */ + public function render(string $layout = null, array $context = []); +} diff --git a/system/src/Grav/Framework/Logger/Processors/UserProcessor.php b/system/src/Grav/Framework/Logger/Processors/UserProcessor.php new file mode 100644 index 0000000..885b4f0 --- /dev/null +++ b/system/src/Grav/Framework/Logger/Processors/UserProcessor.php @@ -0,0 +1,34 @@ +exists()) { + $record['extra']['user'] = ['username' => $user->username, 'email' => $user->email]; + } + + return $record; + } +} diff --git a/system/src/Grav/Framework/Media/Interfaces/MediaCollectionInterface.php b/system/src/Grav/Framework/Media/Interfaces/MediaCollectionInterface.php new file mode 100644 index 0000000..a3587d8 --- /dev/null +++ b/system/src/Grav/Framework/Media/Interfaces/MediaCollectionInterface.php @@ -0,0 +1,17 @@ + ['mime/type', 'mime/type2']] + */ + public static function createFromMimes(array $mimes): self + { + $extensions = []; + foreach ($mimes as $ext => $list) { + foreach ($list as $mime) { + $list = $extensions[$mime] ?? []; + if (!in_array($ext, $list, true)) { + $list[] = $ext; + $extensions[$mime] = $list; + } + } + } + + return new static($extensions, $mimes); + } + + /** + * @param string $extension + * @return string|null + */ + public function getMimeType(string $extension): ?string + { + $extension = $this->cleanInput($extension); + + return $this->mimes[$extension][0] ?? null; + } + + /** + * @param string $mime + * @return string|null + */ + public function getExtension(string $mime): ?string + { + $mime = $this->cleanInput($mime); + + return $this->extensions[$mime][0] ?? null; + } + + /** + * @param string $extension + * @return array + */ + public function getMimeTypes(string $extension): array + { + $extension = $this->cleanInput($extension); + + return $this->mimes[$extension] ?? []; + } + + /** + * @param string $mime + * @return array + */ + public function getExtensions(string $mime): array + { + $mime = $this->cleanInput($mime); + + return $this->extensions[$mime] ?? []; + } + + /** + * @param string $input + * @return string + */ + protected function cleanInput(string $input): string + { + return strtolower(trim($input)); + } + + /** + * @param array $extensions + * @param array $mimes + */ + protected function __construct(array $extensions, array $mimes) + { + $this->extensions = $extensions; + $this->mimes = $mimes; + } +} diff --git a/system/src/Grav/Framework/Object/Access/ArrayAccessTrait.php b/system/src/Grav/Framework/Object/Access/ArrayAccessTrait.php new file mode 100644 index 0000000..affff03 --- /dev/null +++ b/system/src/Grav/Framework/Object/Access/ArrayAccessTrait.php @@ -0,0 +1,66 @@ +hasProperty($offset); + } + + /** + * Returns the value at specified offset. + * + * @param mixed $offset The offset to retrieve. + * @return mixed Can return all value types. + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->getProperty($offset); + } + + /** + * Assigns a value to the specified offset. + * + * @param mixed $offset The offset to assign the value to. + * @param mixed $value The value to set. + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + $this->setProperty($offset, $value); + } + + /** + * Unsets an offset. + * + * @param mixed $offset The offset to unset. + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + $this->unsetProperty($offset); + } +} diff --git a/system/src/Grav/Framework/Object/Access/NestedArrayAccessTrait.php b/system/src/Grav/Framework/Object/Access/NestedArrayAccessTrait.php new file mode 100644 index 0000000..8365875 --- /dev/null +++ b/system/src/Grav/Framework/Object/Access/NestedArrayAccessTrait.php @@ -0,0 +1,66 @@ +hasNestedProperty($offset); + } + + /** + * Returns the value at specified offset. + * + * @param mixed $offset The offset to retrieve. + * @return mixed Can return all value types. + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->getNestedProperty($offset); + } + + /** + * Assigns a value to the specified offset. + * + * @param mixed $offset The offset to assign the value to. + * @param mixed $value The value to set. + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + $this->setNestedProperty($offset, $value); + } + + /** + * Unsets an offset. + * + * @param mixed $offset The offset to unset. + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + $this->unsetNestedProperty($offset); + } +} diff --git a/system/src/Grav/Framework/Object/Access/NestedPropertyCollectionTrait.php b/system/src/Grav/Framework/Object/Access/NestedPropertyCollectionTrait.php new file mode 100644 index 0000000..120c1a4 --- /dev/null +++ b/system/src/Grav/Framework/Object/Access/NestedPropertyCollectionTrait.php @@ -0,0 +1,120 @@ +getIterator() as $id => $element) { + $list[$id] = $element->hasNestedProperty($property, $separator); + } + + return $list; + } + + /** + * @param string $property Object property to be fetched. + * @param mixed $default Default value if not set. + * @param string|null $separator Separator, defaults to '.' + * @return array Key/Value pairs of the properties. + */ + public function getNestedProperty($property, $default = null, $separator = null) + { + $list = []; + + /** @var NestedObjectInterface $element */ + foreach ($this->getIterator() as $id => $element) { + $list[$id] = $element->getNestedProperty($property, $default, $separator); + } + + return $list; + } + + /** + * @param string $property Object property to be updated. + * @param mixed $value New value. + * @param string|null $separator Separator, defaults to '.' + * @return $this + */ + public function setNestedProperty($property, $value, $separator = null) + { + /** @var NestedObjectInterface $element */ + foreach ($this->getIterator() as $element) { + $element->setNestedProperty($property, $value, $separator); + } + + return $this; + } + + /** + * @param string $property Object property to be updated. + * @param string|null $separator Separator, defaults to '.' + * @return $this + */ + public function unsetNestedProperty($property, $separator = null) + { + /** @var NestedObjectInterface $element */ + foreach ($this->getIterator() as $element) { + $element->unsetNestedProperty($property, $separator); + } + + return $this; + } + + /** + * @param string $property Object property to be updated. + * @param string $default Default value. + * @param string|null $separator Separator, defaults to '.' + * @return $this + */ + public function defNestedProperty($property, $default, $separator = null) + { + /** @var NestedObjectInterface $element */ + foreach ($this->getIterator() as $element) { + $element->defNestedProperty($property, $default, $separator); + } + + return $this; + } + + /** + * Group items in the collection by a field. + * + * @param string $property Object property to be used to make groups. + * @param string|null $separator Separator, defaults to '.' + * @return array + */ + public function group($property, $separator = null) + { + $list = []; + + /** @var NestedObjectInterface $element */ + foreach ($this->getIterator() as $element) { + $list[(string) $element->getNestedProperty($property, null, $separator)][] = $element; + } + + return $list; + } +} diff --git a/system/src/Grav/Framework/Object/Access/NestedPropertyTrait.php b/system/src/Grav/Framework/Object/Access/NestedPropertyTrait.php new file mode 100644 index 0000000..7409f4c --- /dev/null +++ b/system/src/Grav/Framework/Object/Access/NestedPropertyTrait.php @@ -0,0 +1,180 @@ +getNestedProperty($property, $test, $separator) !== $test; + } + + /** + * @param string $property Object property to be fetched. + * @param mixed|null $default Default value if property has not been set. + * @param string|null $separator Separator, defaults to '.' + * @return mixed Property value. + */ + public function getNestedProperty($property, $default = null, $separator = null) + { + $separator = $separator ?: '.'; + $path = explode($separator, $property) ?: []; + $offset = array_shift($path) ?? ''; + + if (!$this->hasProperty($offset)) { + return $default; + } + + $current = $this->getProperty($offset); + + while ($path) { + // Get property of nested Object. + if ($current instanceof ObjectInterface) { + if (method_exists($current, 'getNestedProperty')) { + return $current->getNestedProperty(implode($separator, $path), $default, $separator); + } + return $current->getProperty(implode($separator, $path), $default); + } + + $offset = array_shift($path); + + if ((is_array($current) || is_a($current, 'ArrayAccess')) && isset($current[$offset])) { + $current = $current[$offset]; + } elseif (is_object($current) && isset($current->{$offset})) { + $current = $current->{$offset}; + } else { + return $default; + } + }; + + return $current; + } + + + /** + * @param string $property Object property to be updated. + * @param mixed $value New value. + * @param string|null $separator Separator, defaults to '.' + * @return $this + * @throws RuntimeException + */ + public function setNestedProperty($property, $value, $separator = null) + { + $separator = $separator ?: '.'; + $path = explode($separator, $property) ?: []; + $offset = array_shift($path) ?? ''; + + if (!$path) { + $this->setProperty($offset, $value); + + return $this; + } + + $current = &$this->doGetProperty($offset, null, true); + + while ($path) { + $offset = array_shift($path); + + // Handle arrays and scalars. + if ($current === null) { + $current = [$offset => []]; + } elseif (is_array($current)) { + if (!isset($current[$offset])) { + $current[$offset] = []; + } + } else { + throw new RuntimeException("Cannot set nested property {$property} on non-array value"); + } + + $current = &$current[$offset]; + }; + + $current = $value; + + return $this; + } + + /** + * @param string $property Object property to be updated. + * @param string|null $separator Separator, defaults to '.' + * @return $this + * @throws RuntimeException + */ + public function unsetNestedProperty($property, $separator = null) + { + $separator = $separator ?: '.'; + $path = explode($separator, $property) ?: []; + $offset = array_shift($path) ?? ''; + + if (!$path) { + $this->unsetProperty($offset); + + return $this; + } + + $last = array_pop($path); + $current = &$this->doGetProperty($offset, null, true); + + while ($path) { + $offset = array_shift($path); + + // Handle arrays and scalars. + if ($current === null) { + return $this; + } + if (is_array($current)) { + if (!isset($current[$offset])) { + return $this; + } + } else { + throw new RuntimeException("Cannot unset nested property {$property} on non-array value"); + } + + $current = &$current[$offset]; + }; + + unset($current[$last]); + + return $this; + } + + /** + * @param string $property Object property to be updated. + * @param mixed $default Default value. + * @param string|null $separator Separator, defaults to '.' + * @return $this + * @throws RuntimeException + */ + public function defNestedProperty($property, $default, $separator = null) + { + if (!$this->hasNestedProperty($property, $separator)) { + $this->setNestedProperty($property, $default, $separator); + } + + return $this; + } +} diff --git a/system/src/Grav/Framework/Object/Access/OverloadedPropertyTrait.php b/system/src/Grav/Framework/Object/Access/OverloadedPropertyTrait.php new file mode 100644 index 0000000..0f0f89f --- /dev/null +++ b/system/src/Grav/Framework/Object/Access/OverloadedPropertyTrait.php @@ -0,0 +1,62 @@ +hasProperty($offset); + } + + /** + * Returns the value at specified offset. + * + * @param mixed $offset The offset to retrieve. + * @return mixed Can return all value types. + */ + public function __get($offset) + { + return $this->getProperty($offset); + } + + /** + * Assigns a value to the specified offset. + * + * @param mixed $offset The offset to assign the value to. + * @param mixed $value The value to set. + * @return void + */ + public function __set($offset, $value) + { + $this->setProperty($offset, $value); + } + + /** + * Magic method to unset the attribute + * + * @param mixed $offset The name value to unset + * @return void + */ + public function __unset($offset) + { + $this->unsetProperty($offset); + } +} diff --git a/system/src/Grav/Framework/Object/ArrayObject.php b/system/src/Grav/Framework/Object/ArrayObject.php new file mode 100644 index 0000000..2bbe937 --- /dev/null +++ b/system/src/Grav/Framework/Object/ArrayObject.php @@ -0,0 +1,30 @@ +getTypePrefix() : ''; + + if (static::$type) { + return $type . static::$type; + } + + $class = get_class($this); + return $type . strtolower(substr($class, strrpos($class, '\\') + 1)); + } + + /** + * @return string + */ + public function getKey() + { + return $this->_key ?: $this->getType() . '@@' . spl_object_hash($this); + } + + /** + * @return bool + */ + public function hasKey() + { + return !empty($this->_key); + } + + /** + * @param string $property Object property name. + * @return bool[] True if property has been defined (can be null). + */ + public function hasProperty($property) + { + return $this->doHasProperty($property); + } + + /** + * @param string $property Object property to be fetched. + * @param mixed $default Default value if property has not been set. + * @return mixed[] Property values. + */ + public function getProperty($property, $default = null) + { + return $this->doGetProperty($property, $default); + } + + /** + * @param string $property Object property to be updated. + * @param mixed $value New value. + * @return $this + */ + public function setProperty($property, $value) + { + $this->doSetProperty($property, $value); + + return $this; + } + + /** + * @param string $property Object property to be unset. + * @return $this + */ + public function unsetProperty($property) + { + $this->doUnsetProperty($property); + + return $this; + } + + /** + * @param string $property Object property to be defined. + * @param mixed $default Default value. + * @return $this + */ + public function defProperty($property, $default) + { + if (!$this->hasProperty($property)) { + $this->setProperty($property, $default); + } + + return $this; + } + + /** + * @return array + */ + final public function __serialize(): array + { + return $this->doSerialize(); + } + + /** + * @param array $data + * @return void + */ + final public function __unserialize(array $data): void + { + if (method_exists($this, 'initObjectProperties')) { + $this->initObjectProperties(); + } + + $this->doUnserialize($data); + } + + + /** + * @return array + */ + protected function doSerialize() + { + return [ + 'key' => $this->getKey(), + 'type' => $this->getType(), + 'elements' => $this->getElements() + ]; + } + + /** + * @param array $data + * @return void + */ + protected function doUnserialize(array $data) + { + if (!isset($data['key'], $data['type'], $data['elements']) || $data['type'] !== $this->getType()) { + throw new \InvalidArgumentException("Cannot unserialize '{$this->getType()}': Bad data"); + } + + $this->setKey($data['key']); + $this->setElements($data['elements']); + } + + /** + * Implements JsonSerializable interface. + * + * @return array + */ + public function jsonSerialize() + { + return $this->doSerialize(); + } + + /** + * Returns a string representation of this object. + * + * @return string + */ + public function __toString() + { + return $this->getKey(); + } + + /** + * @param string $key + * @return $this + */ + public function setKey($key) + { + $this->_key = (string) $key; + + return $this; + } + + /** + * Create a copy from this collection by cloning all objects in the collection. + */ + public function copy() + { + $list = []; + foreach ($this->getIterator() as $key => $value) { + $list[$key] = is_object($value) ? clone $value : $value; + } + + return $this->createFrom($list); + } + + /** + * @return string[] + */ + public function getObjectKeys() + { + return $this->call('getKey'); + } + + /** + * @param string $property Object property to be matched. + * @return bool[] Key/Value pairs of the properties. + */ + public function doHasProperty($property) + { + $list = []; + + /** @var ObjectInterface $element */ + foreach ($this->getIterator() as $id => $element) { + $list[$id] = (bool)$element->hasProperty($property); + } + + return $list; + } + + /** + * @param string $property Object property to be fetched. + * @param mixed $default Default value if not set. + * @param bool $doCreate Not being used. + * @return mixed[] Key/Value pairs of the properties. + */ + public function &doGetProperty($property, $default = null, $doCreate = false) + { + $list = []; + + /** @var ObjectInterface $element */ + foreach ($this->getIterator() as $id => $element) { + $list[$id] = $element->getProperty($property, $default); + } + + return $list; + } + + /** + * @param string $property Object property to be updated. + * @param mixed $value New value. + * @return $this + */ + public function doSetProperty($property, $value) + { + /** @var ObjectInterface $element */ + foreach ($this->getIterator() as $element) { + $element->setProperty($property, $value); + } + + return $this; + } + + /** + * @param string $property Object property to be updated. + * @return $this + */ + public function doUnsetProperty($property) + { + /** @var ObjectInterface $element */ + foreach ($this->getIterator() as $element) { + $element->unsetProperty($property); + } + + return $this; + } + + /** + * @param string $property Object property to be updated. + * @param mixed $default Default value. + * @return $this + */ + public function doDefProperty($property, $default) + { + /** @var ObjectInterface $element */ + foreach ($this->getIterator() as $element) { + $element->defProperty($property, $default); + } + + return $this; + } + + /** + * @param string $method Method name. + * @param array $arguments List of arguments passed to the function. + * @return mixed[] Return values. + */ + public function call($method, array $arguments = []) + { + $list = []; + + /** + * @var string|int $id + * @var ObjectInterface $element + */ + foreach ($this->getIterator() as $id => $element) { + $callable = [$element, $method]; + $list[$id] = is_callable($callable) ? call_user_func_array($callable, $arguments) : null; + } + + return $list; + } + + /** + * Group items in the collection by a field and return them as associated array. + * + * @param string $property + * @return array + */ + public function group($property) + { + $list = []; + + /** @var ObjectInterface $element */ + foreach ($this->getIterator() as $element) { + $list[(string) $element->getProperty($property)][] = $element; + } + + return $list; + } + + /** + * Group items in the collection by a field and return them as associated array of collections. + * + * @param string $property + * @return static[] + */ + public function collectionGroup($property) + { + $collections = []; + foreach ($this->group($property) as $id => $elements) { + /** @var static $collection */ + $collection = $this->createFrom($elements); + + $collections[$id] = $collection; + } + + return $collections; + } +} diff --git a/system/src/Grav/Framework/Object/Base/ObjectTrait.php b/system/src/Grav/Framework/Object/Base/ObjectTrait.php new file mode 100644 index 0000000..d4e324b --- /dev/null +++ b/system/src/Grav/Framework/Object/Base/ObjectTrait.php @@ -0,0 +1,200 @@ +getTypePrefix() : ''; + + if (static::$type) { + return $type . static::$type; + } + + $class = get_class($this); + return $type . strtolower(substr($class, strrpos($class, '\\') + 1)); + } + + /** + * @return string + */ + public function getKey() + { + return $this->_key ?: $this->getType() . '@@' . spl_object_hash($this); + } + + /** + * @return bool + */ + public function hasKey() + { + return !empty($this->_key); + } + + /** + * @param string $property Object property name. + * @return bool True if property has been defined (can be null). + */ + public function hasProperty($property) + { + return $this->doHasProperty($property); + } + + /** + * @param string $property Object property to be fetched. + * @param mixed $default Default value if property has not been set. + * @return mixed Property value. + */ + public function getProperty($property, $default = null) + { + return $this->doGetProperty($property, $default); + } + + /** + * @param string $property Object property to be updated. + * @param mixed $value New value. + * @return $this + */ + public function setProperty($property, $value) + { + $this->doSetProperty($property, $value); + + return $this; + } + + /** + * @param string $property Object property to be unset. + * @return $this + */ + public function unsetProperty($property) + { + $this->doUnsetProperty($property); + + return $this; + } + + /** + * @param string $property Object property to be defined. + * @param mixed $default Default value. + * @return $this + */ + public function defProperty($property, $default) + { + if (!$this->hasProperty($property)) { + $this->setProperty($property, $default); + } + + return $this; + } + + /** + * @return array + */ + final public function __serialize(): array + { + return $this->doSerialize(); + } + + /** + * @param array $data + * @return void + */ + final public function __unserialize(array $data): void + { + if (method_exists($this, 'initObjectProperties')) { + $this->initObjectProperties(); + } + + $this->doUnserialize($data); + } + + /** + * @return array + */ + protected function doSerialize() + { + return ['key' => $this->getKey(), 'type' => $this->getType(), 'elements' => $this->getElements()]; + } + + /** + * @param array $serialized + * @return void + */ + protected function doUnserialize(array $serialized) + { + if (!isset($serialized['key'], $serialized['type'], $serialized['elements']) || $serialized['type'] !== $this->getType()) { + throw new InvalidArgumentException("Cannot unserialize '{$this->getType()}': Bad data"); + } + + $this->setKey($serialized['key']); + $this->setElements($serialized['elements']); + } + + /** + * Implements JsonSerializable interface. + * + * @return array + */ + public function jsonSerialize() + { + return $this->doSerialize(); + } + + /** + * Returns a string representation of this object. + * + * @return string + */ + public function __toString() + { + return $this->getKey(); + } + + /** + * @param string $key + * @return $this + */ + protected function setKey($key) + { + $this->_key = (string) $key; + + return $this; + } +} diff --git a/system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php b/system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php new file mode 100644 index 0000000..d8f1524 --- /dev/null +++ b/system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php @@ -0,0 +1,239 @@ +{$accessor}(); + break; + } + } + + if ($op) { + $function = 'filter' . ucfirst(strtolower($op)); + if (method_exists(static::class, $function)) { + $value = static::$function($value); + } + } + + return $value; + } + + /** + * @param string $str + * @return string + */ + public static function filterLower($str) + { + return mb_strtolower($str); + } + + /** + * @param string $str + * @return string + */ + public static function filterUpper($str) + { + return mb_strtoupper($str); + } + + /** + * @param string $str + * @return int + */ + public static function filterLength($str) + { + return mb_strlen($str); + } + + /** + * @param string $str + * @return string + */ + public static function filterLtrim($str) + { + return ltrim($str); + } + + /** + * @param string $str + * @return string + */ + public static function filterRtrim($str) + { + return rtrim($str); + } + + /** + * @param string $str + * @return string + */ + public static function filterTrim($str) + { + return trim($str); + } + + /** + * Helper for sorting arrays of objects based on multiple fields + orientations. + * + * Comparison between two strings is natural and case insensitive. + * + * @param string $name + * @param int $orientation + * @param Closure|null $next + * + * @return Closure + */ + public static function sortByField($name, $orientation = 1, Closure $next = null) + { + if (!$next) { + $next = function ($a, $b) { + return 0; + }; + } + + return function ($a, $b) use ($name, $next, $orientation) { + $aValue = static::getObjectFieldValue($a, $name); + $bValue = static::getObjectFieldValue($b, $name); + + if ($aValue === $bValue) { + return $next($a, $b); + } + + // For strings we use natural case insensitive sorting. + if (is_string($aValue) && is_string($bValue)) { + return strnatcasecmp($aValue, $bValue) * $orientation; + } + + return (($aValue > $bValue) ? 1 : -1) * $orientation; + }; + } + + /** + * {@inheritDoc} + */ + public function walkComparison(Comparison $comparison) + { + $field = $comparison->getField(); + $value = $comparison->getValue()->getValue(); // shortcut for walkValue() + + switch ($comparison->getOperator()) { + case Comparison::EQ: + return function ($object) use ($field, $value) { + return static::getObjectFieldValue($object, $field) === $value; + }; + + case Comparison::NEQ: + return function ($object) use ($field, $value) { + return static::getObjectFieldValue($object, $field) !== $value; + }; + + case Comparison::LT: + return function ($object) use ($field, $value) { + return static::getObjectFieldValue($object, $field) < $value; + }; + + case Comparison::LTE: + return function ($object) use ($field, $value) { + return static::getObjectFieldValue($object, $field) <= $value; + }; + + case Comparison::GT: + return function ($object) use ($field, $value) { + return static::getObjectFieldValue($object, $field) > $value; + }; + + case Comparison::GTE: + return function ($object) use ($field, $value) { + return static::getObjectFieldValue($object, $field) >= $value; + }; + + case Comparison::IN: + return function ($object) use ($field, $value) { + return in_array(static::getObjectFieldValue($object, $field), $value, true); + }; + + case Comparison::NIN: + return function ($object) use ($field, $value) { + return !in_array(static::getObjectFieldValue($object, $field), $value, true); + }; + + case Comparison::CONTAINS: + return function ($object) use ($field, $value) { + return false !== strpos(static::getObjectFieldValue($object, $field), $value); + }; + + case Comparison::MEMBER_OF: + return function ($object) use ($field, $value) { + $fieldValues = static::getObjectFieldValue($object, $field); + if (!is_array($fieldValues)) { + $fieldValues = iterator_to_array($fieldValues); + } + return in_array($value, $fieldValues, true); + }; + + case Comparison::STARTS_WITH: + return function ($object) use ($field, $value) { + return 0 === strpos(static::getObjectFieldValue($object, $field), $value); + }; + + case Comparison::ENDS_WITH: + return function ($object) use ($field, $value) { + return $value === substr(static::getObjectFieldValue($object, $field), -strlen($value)); + }; + + default: + throw new RuntimeException("Unknown comparison operator: " . $comparison->getOperator()); + } + } +} diff --git a/system/src/Grav/Framework/Object/Interfaces/NestedObjectCollectionInterface.php b/system/src/Grav/Framework/Object/Interfaces/NestedObjectCollectionInterface.php new file mode 100644 index 0000000..a2431be --- /dev/null +++ b/system/src/Grav/Framework/Object/Interfaces/NestedObjectCollectionInterface.php @@ -0,0 +1,64 @@ + + */ +interface NestedObjectCollectionInterface extends ObjectCollectionInterface +{ + /** + * @param string $property Object property name. + * @param string|null $separator Separator, defaults to '.' + * @return bool[] List of [key => bool] pairs. + */ + public function hasNestedProperty($property, $separator = null); + + /** + * @param string $property Object property to be fetched. + * @param mixed|null $default Default value if property has not been set. + * @param string|null $separator Separator, defaults to '.' + * @return mixed[] List of [key => value] pairs. + */ + public function getNestedProperty($property, $default = null, $separator = null); + + /** + * @param string $property Object property to be updated. + * @param mixed $value New value. + * @param string|null $separator Separator, defaults to '.' + * @return $this + * @throws RuntimeException + */ + public function setNestedProperty($property, $value, $separator = null); + + /** + * @param string $property Object property to be defined. + * @param mixed $default Default value. + * @param string|null $separator Separator, defaults to '.' + * @return $this + * @throws RuntimeException + */ + public function defNestedProperty($property, $default, $separator = null); + + /** + * @param string $property Object property to be unset. + * @param string|null $separator Separator, defaults to '.' + * @return $this + * @throws RuntimeException + */ + public function unsetNestedProperty($property, $separator = null); +} diff --git a/system/src/Grav/Framework/Object/Interfaces/NestedObjectInterface.php b/system/src/Grav/Framework/Object/Interfaces/NestedObjectInterface.php new file mode 100644 index 0000000..4d7a880 --- /dev/null +++ b/system/src/Grav/Framework/Object/Interfaces/NestedObjectInterface.php @@ -0,0 +1,60 @@ + + * @extends Selectable + */ +interface ObjectCollectionInterface extends CollectionInterface, Selectable, Serializable +{ + /** + * @return string + */ + public function getType(); + + /** + * @return string + */ + public function getKey(); + + /** + * @param string $key + * @return $this + */ + public function setKey($key); + + /** + * @param string $property Object property name. + * @return bool[] List of [key => bool] pairs. + */ + public function hasProperty($property); + + /** + * @param string $property Object property to be fetched. + * @param mixed|null $default Default value if property has not been set. + * @return mixed[] List of [key => value] pairs. + */ + public function getProperty($property, $default = null); + + /** + * @param string $property Object property to be updated. + * @param mixed $value New value. + * @return $this + */ + public function setProperty($property, $value); + + /** + * @param string $property Object property to be defined. + * @param mixed $default Default value. + * @return $this + */ + public function defProperty($property, $default); + + /** + * @param string $property Object property to be unset. + * @return $this + */ + public function unsetProperty($property); + + /** + * Create a copy from this collection by cloning all objects in the collection. + * + * @return static + * @phpstan-return static + */ + public function copy(); + + /** + * @return array + */ + public function getObjectKeys(); + + /** + * @param string $name Method name. + * @param array $arguments List of arguments passed to the function. + * @return array Return values. + */ + public function call($name, array $arguments = []); + + /** + * Group items in the collection by a field and return them as associated array. + * + * @param string $property + * @return array + */ + public function group($property); + + /** + * Group items in the collection by a field and return them as associated array of collections. + * + * @param string $property + * @return static[] + * @phpstan-return array> + */ + public function collectionGroup($property); + + /** + * @param array $ordering + * @return ObjectCollectionInterface + * @phpstan-return static + */ + public function orderBy(array $ordering); + + /** + * @param int $start + * @param int|null $limit + * @return ObjectCollectionInterface + * @phpstan-return static + */ + public function limit($start, $limit = null); +} diff --git a/system/src/Grav/Framework/Object/Interfaces/ObjectInterface.php b/system/src/Grav/Framework/Object/Interfaces/ObjectInterface.php new file mode 100644 index 0000000..e8ec2c1 --- /dev/null +++ b/system/src/Grav/Framework/Object/Interfaces/ObjectInterface.php @@ -0,0 +1,63 @@ + + * @implements NestedObjectCollectionInterface + */ +class ObjectCollection extends ArrayCollection implements NestedObjectCollectionInterface +{ + use ObjectCollectionTrait; + use NestedPropertyCollectionTrait { + NestedPropertyCollectionTrait::group insteadof ObjectCollectionTrait; + } + + /** + * @param array $elements + * @param string|null $key + * @throws InvalidArgumentException + */ + public function __construct(array $elements = [], $key = null) + { + parent::__construct($this->setElements($elements)); + + $this->setKey($key ?? ''); + } + + /** + * @param array $ordering + * @return static + * @phpstan-return static + */ + public function orderBy(array $ordering) + { + $criteria = Criteria::create()->orderBy($ordering); + + return $this->matching($criteria); + } + + /** + * @param int $start + * @param int|null $limit + * @return static + * @phpstan-return static + */ + public function limit($start, $limit = null) + { + return $this->createFrom($this->slice($start, $limit)); + } + + /** + * @param Criteria $criteria + * @return static + * @phpstan-return static + */ + public function matching(Criteria $criteria) + { + $expr = $criteria->getWhereExpression(); + $filtered = $this->getElements(); + + if ($expr) { + $visitor = new ObjectExpressionVisitor(); + $filter = $visitor->dispatch($expr); + $filtered = array_filter($filtered, $filter); + } + + if ($orderings = $criteria->getOrderings()) { + $next = null; + /** + * @var string $field + * @var string $ordering + */ + foreach (array_reverse($orderings) as $field => $ordering) { + $next = ObjectExpressionVisitor::sortByField($field, $ordering === Criteria::DESC ? -1 : 1, $next); + } + + if ($next) { + uasort($filtered, $next); + } + } + + $offset = $criteria->getFirstResult(); + $length = $criteria->getMaxResults(); + + if ($offset || $length) { + $filtered = array_slice($filtered, (int)$offset, $length); + } + + return $this->createFrom($filtered); + } + + /** + * @return array + */ + protected function getElements() + { + return $this->toArray(); + } + + /** + * @param array $elements + * @return array + */ + protected function setElements(array $elements) + { + return $elements; + } +} diff --git a/system/src/Grav/Framework/Object/ObjectIndex.php b/system/src/Grav/Framework/Object/ObjectIndex.php new file mode 100644 index 0000000..50ac427 --- /dev/null +++ b/system/src/Grav/Framework/Object/ObjectIndex.php @@ -0,0 +1,266 @@ + + * @implements NestedObjectCollectionInterface + */ +abstract class ObjectIndex extends AbstractIndexCollection implements NestedObjectCollectionInterface +{ + /** @var string */ + protected static $type; + + /** @var string */ + private $_key; + + /** + * @param bool $prefix + * @return string + */ + public function getType($prefix = true) + { + $type = $prefix ? $this->getTypePrefix() : ''; + + if (static::$type) { + return $type . static::$type; + } + + $class = get_class($this); + return $type . strtolower(substr($class, strrpos($class, '\\') + 1)); + } + + /** + * @return string + */ + public function getKey() + { + return $this->_key ?: $this->getType() . '@@' . spl_object_hash($this); + } + + /** + * @param string $key + * @return $this + */ + public function setKey($key) + { + $this->_key = $key; + + return $this; + } + + /** + * @param string $property Object property name. + * @return bool[] True if property has been defined (can be null). + */ + public function hasProperty($property) + { + return $this->__call('hasProperty', [$property]); + } + + /** + * @param string $property Object property to be fetched. + * @param mixed $default Default value if property has not been set. + * @return mixed[] Property values. + */ + public function getProperty($property, $default = null) + { + return $this->__call('getProperty', [$property, $default]); + } + + /** + * @param string $property Object property to be updated. + * @param string $value New value. + * @return ObjectCollectionInterface + */ + public function setProperty($property, $value) + { + return $this->__call('setProperty', [$property, $value]); + } + + /** + * @param string $property Object property to be defined. + * @param mixed $default Default value. + * @return ObjectCollectionInterface + */ + public function defProperty($property, $default) + { + return $this->__call('defProperty', [$property, $default]); + } + + /** + * @param string $property Object property to be unset. + * @return ObjectCollectionInterface + */ + public function unsetProperty($property) + { + return $this->__call('unsetProperty', [$property]); + } + + /** + * @param string $property Object property name. + * @param string|null $separator Separator, defaults to '.' + * @return bool[] True if property has been defined (can be null). + */ + public function hasNestedProperty($property, $separator = null) + { + return $this->__call('hasNestedProperty', [$property, $separator]); + } + + /** + * @param string $property Object property to be fetched. + * @param mixed $default Default value if property has not been set. + * @param string|null $separator Separator, defaults to '.' + * @return mixed[] Property values. + */ + public function getNestedProperty($property, $default = null, $separator = null) + { + return $this->__call('getNestedProperty', [$property, $default, $separator]); + } + + /** + * @param string $property Object property to be updated. + * @param mixed $value New value. + * @param string|null $separator Separator, defaults to '.' + * @return ObjectCollectionInterface + */ + public function setNestedProperty($property, $value, $separator = null) + { + return $this->__call('setNestedProperty', [$property, $value, $separator]); + } + + /** + * @param string $property Object property to be defined. + * @param mixed $default Default value. + * @param string|null $separator Separator, defaults to '.' + * @return ObjectCollectionInterface + */ + public function defNestedProperty($property, $default, $separator = null) + { + return $this->__call('defNestedProperty', [$property, $default, $separator]); + } + + /** + * @param string $property Object property to be unset. + * @param string|null $separator Separator, defaults to '.' + * @return ObjectCollectionInterface + */ + public function unsetNestedProperty($property, $separator = null) + { + return $this->__call('unsetNestedProperty', [$property, $separator]); + } + + /** + * Create a copy from this collection by cloning all objects in the collection. + * + * @return static + * @return static + */ + public function copy() + { + $list = []; + foreach ($this->getIterator() as $key => $value) { + $list[$key] = is_object($value) ? clone $value : $value; + } + + return $this->createFrom($list); + } + + /** + * @return array + */ + public function getObjectKeys() + { + return $this->getKeys(); + } + + /** + * @param array $ordering + * @return ObjectCollectionInterface + */ + public function orderBy(array $ordering) + { + return $this->__call('orderBy', [$ordering]); + } + + /** + * @param string $method + * @param array $arguments + * @return array|mixed + */ + public function call($method, array $arguments = []) + { + return $this->__call('call', [$method, $arguments]); + } + + /** + * Group items in the collection by a field and return them as associated array. + * + * @param string $property + * @return array + */ + public function group($property) + { + return $this->__call('group', [$property]); + } + + /** + * Group items in the collection by a field and return them as associated array of collections. + * + * @param string $property + * @return ObjectCollectionInterface[] + */ + public function collectionGroup($property) + { + return $this->__call('collectionGroup', [$property]); + } + + /** + * {@inheritDoc} + */ + public function matching(Criteria $criteria) + { + /** @var ObjectCollectionInterface $collection */ + $collection = $this->loadCollection($this->getEntries()); + + return $collection->matching($criteria); + } + + /** + * @param string $name + * @param array $arguments + * @return mixed + */ + abstract public function __call($name, $arguments); + + /** + * @return string + */ + protected function getTypePrefix() + { + return ''; + } +} diff --git a/system/src/Grav/Framework/Object/Property/ArrayPropertyTrait.php b/system/src/Grav/Framework/Object/Property/ArrayPropertyTrait.php new file mode 100644 index 0000000..fa3fed1 --- /dev/null +++ b/system/src/Grav/Framework/Object/Property/ArrayPropertyTrait.php @@ -0,0 +1,115 @@ +setElements($elements); + $this->setKey($key ?? ''); + } + + /** + * @param string $property Object property name. + * @return bool True if property has been defined (can be null). + */ + protected function doHasProperty($property) + { + return array_key_exists($property, $this->_elements); + } + + /** + * @param string $property Object property to be fetched. + * @param mixed $default Default value if property has not been set. + * @param bool $doCreate Set true to create variable. + * @return mixed Property value. + */ + protected function &doGetProperty($property, $default = null, $doCreate = false) + { + if (!array_key_exists($property, $this->_elements)) { + if ($doCreate) { + $this->_elements[$property] = null; + } else { + return $default; + } + } + + return $this->_elements[$property]; + } + + /** + * @param string $property Object property to be updated. + * @param mixed $value New value. + * @return void + */ + protected function doSetProperty($property, $value) + { + $this->_elements[$property] = $value; + } + + /** + * @param string $property Object property to be unset. + * @return void + */ + protected function doUnsetProperty($property) + { + unset($this->_elements[$property]); + } + + /** + * @param string $property + * @param mixed|null $default + * @return mixed|null + */ + protected function getElement($property, $default = null) + { + return array_key_exists($property, $this->_elements) ? $this->_elements[$property] : $default; + } + + /** + * @return array + */ + protected function getElements() + { + return array_filter($this->_elements, function ($val) { + return $val !== null; + }); + } + + /** + * @param array $elements + * @return void + */ + protected function setElements(array $elements) + { + $this->_elements = $elements; + } + + abstract protected function setKey($key); +} diff --git a/system/src/Grav/Framework/Object/Property/LazyPropertyTrait.php b/system/src/Grav/Framework/Object/Property/LazyPropertyTrait.php new file mode 100644 index 0000000..d5baddb --- /dev/null +++ b/system/src/Grav/Framework/Object/Property/LazyPropertyTrait.php @@ -0,0 +1,115 @@ +offsetLoad($offset, $value)` called first time object property gets accessed + * - `$this->offsetPrepare($offset, $value)` called on every object property set + * - `$this->offsetSerialize($offset, $value)` called when the raw or serialized object property value is needed + * + * @package Grav\Framework\Object\Property + */ +trait LazyPropertyTrait +{ + use ArrayPropertyTrait, ObjectPropertyTrait { + ObjectPropertyTrait::__construct insteadof ArrayPropertyTrait; + ArrayPropertyTrait::doHasProperty as hasArrayProperty; + ArrayPropertyTrait::doGetProperty as getArrayProperty; + ArrayPropertyTrait::doSetProperty as setArrayProperty; + ArrayPropertyTrait::doUnsetProperty as unsetArrayProperty; + ArrayPropertyTrait::getElement as getArrayElement; + ArrayPropertyTrait::getElements as getArrayElements; + ArrayPropertyTrait::setElements insteadof ObjectPropertyTrait; + ObjectPropertyTrait::doHasProperty as hasObjectProperty; + ObjectPropertyTrait::doGetProperty as getObjectProperty; + ObjectPropertyTrait::doSetProperty as setObjectProperty; + ObjectPropertyTrait::doUnsetProperty as unsetObjectProperty; + ObjectPropertyTrait::getElement as getObjectElement; + ObjectPropertyTrait::getElements as getObjectElements; + } + + /** + * @param string $property Object property name. + * @return bool True if property has been defined (can be null). + */ + protected function doHasProperty($property) + { + return $this->hasArrayProperty($property) || $this->hasObjectProperty($property); + } + + /** + * @param string $property Object property to be fetched. + * @param mixed $default Default value if property has not been set. + * @param bool $doCreate + * @return mixed Property value. + */ + protected function &doGetProperty($property, $default = null, $doCreate = false) + { + if ($this->hasObjectProperty($property)) { + return $this->getObjectProperty($property, $default, function ($default = null) use ($property) { + return $this->getArrayProperty($property, $default); + }); + } + + return $this->getArrayProperty($property, $default, $doCreate); + } + + /** + * @param string $property Object property to be updated. + * @param mixed $value New value. + * @return void + */ + protected function doSetProperty($property, $value) + { + if ($this->hasObjectProperty($property)) { + $this->setObjectProperty($property, $value); + } else { + $this->setArrayProperty($property, $value); + } + } + + /** + * @param string $property Object property to be unset. + * @return void + */ + protected function doUnsetProperty($property) + { + $this->hasObjectProperty($property) ? + $this->unsetObjectProperty($property) : $this->unsetArrayProperty($property); + } + + /** + * @param string $property + * @param mixed|null $default + * @return mixed|null + */ + protected function getElement($property, $default = null) + { + if ($this->isPropertyLoaded($property)) { + return $this->getObjectElement($property, $default); + } + + return $this->getArrayElement($property, $default); + } + + /** + * @return array + */ + protected function getElements() + { + return $this->getObjectElements() + $this->getArrayElements(); + } +} diff --git a/system/src/Grav/Framework/Object/Property/MixedPropertyTrait.php b/system/src/Grav/Framework/Object/Property/MixedPropertyTrait.php new file mode 100644 index 0000000..584e812 --- /dev/null +++ b/system/src/Grav/Framework/Object/Property/MixedPropertyTrait.php @@ -0,0 +1,121 @@ +offsetLoad($offset, $value)` called first time object property gets accessed + * - `$this->offsetPrepare($offset, $value)` called on every object property set + * - `$this->offsetSerialize($offset, $value)` called when the raw or serialized object property value is needed + + * + * @package Grav\Framework\Object\Property + */ +trait MixedPropertyTrait +{ + use ArrayPropertyTrait, ObjectPropertyTrait { + ObjectPropertyTrait::__construct insteadof ArrayPropertyTrait; + ArrayPropertyTrait::doHasProperty as hasArrayProperty; + ArrayPropertyTrait::doGetProperty as getArrayProperty; + ArrayPropertyTrait::doSetProperty as setArrayProperty; + ArrayPropertyTrait::doUnsetProperty as unsetArrayProperty; + ArrayPropertyTrait::getElement as getArrayElement; + ArrayPropertyTrait::getElements as getArrayElements; + ArrayPropertyTrait::setElements as setArrayElements; + ObjectPropertyTrait::doHasProperty as hasObjectProperty; + ObjectPropertyTrait::doGetProperty as getObjectProperty; + ObjectPropertyTrait::doSetProperty as setObjectProperty; + ObjectPropertyTrait::doUnsetProperty as unsetObjectProperty; + ObjectPropertyTrait::getElement as getObjectElement; + ObjectPropertyTrait::getElements as getObjectElements; + ObjectPropertyTrait::setElements as setObjectElements; + } + + /** + * @param string $property Object property name. + * @return bool True if property has been defined (can be null). + */ + protected function doHasProperty($property) + { + return $this->hasArrayProperty($property) || $this->hasObjectProperty($property); + } + + /** + * @param string $property Object property to be fetched. + * @param mixed $default Default value if property has not been set. + * @param bool $doCreate + * @return mixed Property value. + */ + protected function &doGetProperty($property, $default = null, $doCreate = false) + { + if ($this->hasObjectProperty($property)) { + return $this->getObjectProperty($property); + } + + return $this->getArrayProperty($property, $default, $doCreate); + } + + /** + * @param string $property Object property to be updated. + * @param mixed $value New value. + * @return void + */ + protected function doSetProperty($property, $value) + { + $this->hasObjectProperty($property) + ? $this->setObjectProperty($property, $value) : $this->setArrayProperty($property, $value); + } + + /** + * @param string $property Object property to be unset. + * @return void + */ + protected function doUnsetProperty($property) + { + $this->hasObjectProperty($property) ? + $this->unsetObjectProperty($property) : $this->unsetArrayProperty($property); + } + + /** + * @param string $property + * @param mixed|null $default + * @return mixed|null + */ + protected function getElement($property, $default = null) + { + if ($this->hasObjectProperty($property)) { + return $this->getObjectElement($property, $default); + } + + return $this->getArrayElement($property, $default); + } + + /** + * @return array + */ + protected function getElements() + { + return $this->getObjectElements() + $this->getArrayElements(); + } + + /** + * @param array $elements + * @return void + */ + protected function setElements(array $elements) + { + $this->setObjectElements(array_intersect_key($elements, $this->_definedProperties)); + $this->setArrayElements(array_diff_key($elements, $this->_definedProperties)); + } +} diff --git a/system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php b/system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php new file mode 100644 index 0000000..435220a --- /dev/null +++ b/system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php @@ -0,0 +1,213 @@ +offsetLoad($offset, $value)` called first time object property gets accessed + * - `$this->offsetPrepare($offset, $value)` called on every object property set + * - `$this->offsetSerialize($offset, $value)` called when the raw or serialized object property value is needed + * + * @package Grav\Framework\Object\Property + */ +trait ObjectPropertyTrait +{ + /** @var array */ + private $_definedProperties; + + /** + * @param array $elements + * @param string|null $key + * @throws InvalidArgumentException + */ + public function __construct(array $elements = [], $key = null) + { + $this->initObjectProperties(); + $this->setElements($elements); + $this->setKey($key ?? ''); + } + + /** + * @param string $property Object property name. + * @return bool True if property has been loaded. + */ + protected function isPropertyLoaded($property) + { + return !empty($this->_definedProperties[$property]); + } + + /** + * @param string $offset + * @param mixed $value + * @return mixed + */ + protected function offsetLoad($offset, $value) + { + $methodName = "offsetLoad_{$offset}"; + + return method_exists($this, $methodName)? $this->{$methodName}($value) : $value; + } + + /** + * @param string $offset + * @param mixed $value + * @return mixed + */ + protected function offsetPrepare($offset, $value) + { + $methodName = "offsetPrepare_{$offset}"; + + return method_exists($this, $methodName) ? $this->{$methodName}($value) : $value; + } + + /** + * @param string $offset + * @param mixed $value + * @return mixed + */ + protected function offsetSerialize($offset, $value) + { + $methodName = "offsetSerialize_{$offset}"; + + return method_exists($this, $methodName) ? $this->{$methodName}($value) : $value; + } + + /** + * @param string $property Object property name. + * @return bool True if property has been defined (can be null). + */ + protected function doHasProperty($property) + { + return array_key_exists($property, $this->_definedProperties); + } + + /** + * @param string $property Object property to be fetched. + * @param mixed $default Default value if property has not been set. + * @param callable|bool $doCreate Set true to create variable. + * @return mixed Property value. + */ + protected function &doGetProperty($property, $default = null, $doCreate = false) + { + if (!array_key_exists($property, $this->_definedProperties)) { + throw new InvalidArgumentException("Property '{$property}' does not exist in the object!"); + } + + if (empty($this->_definedProperties[$property])) { + if ($doCreate === true) { + $this->_definedProperties[$property] = true; + $this->{$property} = null; + } elseif (is_callable($doCreate)) { + $this->_definedProperties[$property] = true; + $this->{$property} = $this->offsetLoad($property, $doCreate()); + } else { + return $default; + } + } + + return $this->{$property}; + } + + /** + * @param string $property Object property to be updated. + * @param mixed $value New value. + * @return void + * @throws InvalidArgumentException + */ + protected function doSetProperty($property, $value) + { + if (!array_key_exists($property, $this->_definedProperties)) { + throw new InvalidArgumentException("Property '{$property}' does not exist in the object!"); + } + + $this->_definedProperties[$property] = true; + $this->{$property} = $this->offsetPrepare($property, $value); + } + + /** + * @param string $property Object property to be unset. + * @return void + */ + protected function doUnsetProperty($property) + { + if (!array_key_exists($property, $this->_definedProperties)) { + return; + } + + $this->_definedProperties[$property] = false; + $this->{$property} = null; + } + + /** + * @return void + */ + protected function initObjectProperties() + { + $this->_definedProperties = []; + foreach (get_object_vars($this) as $property => $value) { + if ($property[0] !== '_') { + $this->_definedProperties[$property] = ($value !== null); + } + } + } + + /** + * @param string $property + * @param mixed|null $default + * @return mixed|null + */ + protected function getElement($property, $default = null) + { + if (empty($this->_definedProperties[$property])) { + return $default; + } + + return $this->offsetSerialize($property, $this->{$property}); + } + + /** + * @return array + */ + protected function getElements() + { + $properties = array_intersect_key(get_object_vars($this), array_filter($this->_definedProperties)); + + $elements = []; + foreach ($properties as $offset => $value) { + $serialized = $this->offsetSerialize($offset, $value); + if ($serialized !== null) { + $elements[$offset] = $this->offsetSerialize($offset, $value); + } + } + + return $elements; + } + + /** + * @param array $elements + * @return void + */ + protected function setElements(array $elements) + { + foreach ($elements as $property => $value) { + $this->setProperty($property, $value); + } + } +} diff --git a/system/src/Grav/Framework/Object/PropertyObject.php b/system/src/Grav/Framework/Object/PropertyObject.php new file mode 100644 index 0000000..dbeb5c4 --- /dev/null +++ b/system/src/Grav/Framework/Object/PropertyObject.php @@ -0,0 +1,30 @@ + 'page', + 'limit' => 10, + 'display' => 5, + 'opening' => 0, + 'ending' => 0, + 'url' => null, + 'param' => null, + 'use_query_param' => false + ]; + /** @var array */ + private $items; + + /** + * @return bool + */ + public function isEnabled(): bool + { + return $this->count() > 1; + } + + /** + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * @return Route|null + */ + public function getRoute(): ?Route + { + return $this->route; + } + + /** + * @return int + */ + public function getTotalPages(): int + { + return $this->pages; + } + + /** + * @return int + */ + public function getPageNumber(): int + { + return $this->page ?? 1; + } + + /** + * @param int $count + * @return int|null + */ + public function getPrevNumber(int $count = 1): ?int + { + $page = $this->page - $count; + + return $page >= 1 ? $page : null; + } + + /** + * @param int $count + * @return int|null + */ + public function getNextNumber(int $count = 1): ?int + { + $page = $this->page + $count; + + return $page <= $this->pages ? $page : null; + } + + /** + * @param int $page + * @param string|null $label + * @return PaginationPage|null + */ + public function getPage(int $page, string $label = null): ?PaginationPage + { + if ($page < 1 || $page > $this->pages) { + return null; + } + + $start = ($page - 1) * $this->limit; + $type = $this->getOptions()['type']; + $param = $this->getOptions()['param']; + $useQuery = $this->getOptions()['use_query_param']; + if ($type === 'page') { + $param = $param ?? 'page'; + $offset = $page; + } else { + $param = $param ?? 'start'; + $offset = $start; + } + + if ($useQuery) { + $route = $this->route->withQueryParam($param, $offset); + } else { + $route = $this->route->withGravParam($param, $offset); + } + + return new PaginationPage( + [ + 'label' => $label ?? (string)$page, + 'number' => $page, + 'offset_start' => $start, + 'offset_end' => min($start + $this->limit, $this->total) - 1, + 'enabled' => $page !== $this->page || $this->viewAll, + 'active' => $page === $this->page, + 'route' => $route + ] + ); + } + + /** + * @param string|null $label + * @param int $count + * @return PaginationPage|null + */ + public function getFirstPage(string $label = null, int $count = 0): ?PaginationPage + { + return $this->getPage(1 + $count, $label ?? $this->getOptions()['label_first'] ?? null); + } + + /** + * @param string|null $label + * @param int $count + * @return PaginationPage|null + */ + public function getPrevPage(string $label = null, int $count = 1): ?PaginationPage + { + return $this->getPage($this->page - $count, $label ?? $this->getOptions()['label_prev'] ?? null); + } + + /** + * @param string|null $label + * @param int $count + * @return PaginationPage|null + */ + public function getNextPage(string $label = null, int $count = 1): ?PaginationPage + { + return $this->getPage($this->page + $count, $label ?? $this->getOptions()['label_next'] ?? null); + } + + /** + * @param string|null $label + * @param int $count + * @return PaginationPage|null + */ + public function getLastPage(string $label = null, int $count = 0): ?PaginationPage + { + return $this->getPage($this->pages - $count, $label ?? $this->getOptions()['label_last'] ?? null); + } + + /** + * @return int + */ + public function getStart(): int + { + return $this->start ?? 0; + } + + /** + * @return int + */ + public function getLimit(): int + { + return $this->limit; + } + + /** + * @return int + */ + public function getTotal(): int + { + return $this->total; + } + + /** + * @return int + */ + public function count(): int + { + $this->loadItems(); + + return count($this->items); + } + + /** + * @return ArrayIterator + */ + public function getIterator() + { + $this->loadItems(); + + return new ArrayIterator($this->items); + } + + /** + * @return array + */ + public function getPages(): array + { + $this->loadItems(); + + return $this->items; + } + + /** + * @return void + */ + protected function loadItems() + { + $this->calculateRange(); + + // Make list like: 1 ... 4 5 6 ... 10 + $range = range($this->pagesStart, $this->pagesStop); + //$range[] = 1; + //$range[] = $this->pages; + natsort($range); + $range = array_unique($range); + + $this->items = []; + foreach ($range as $i) { + $this->items[$i] = $this->getPage($i); + } + } + + /** + * @param Route $route + * @return $this + */ + protected function setRoute(Route $route) + { + $this->route = $route; + + return $this; + } + + /** + * @param array|null $options + * @return $this + */ + protected function setOptions(array $options = null) + { + $this->options = $options ? array_merge($this->defaultOptions, $options) : $this->defaultOptions; + + return $this; + } + + /** + * @param int|null $page + * @return $this + */ + protected function setPage(int $page = null) + { + $this->page = (int)max($page, 1); + $this->start = null; + + return $this; + } + + /** + * @param int|null $start + * @return $this + */ + protected function setStart(int $start = null) + { + $this->start = (int)max($start, 0); + $this->page = null; + + return $this; + } + + /** + * @param int|null $limit + * @return $this + */ + protected function setLimit(int $limit = null) + { + $this->limit = (int)max($limit ?? $this->getOptions()['limit'], 0); + + // No limit, display all records in a single page. + $this->viewAll = !$limit; + + return $this; + } + + /** + * @param int $total + * @return $this + */ + protected function setTotal(int $total) + { + $this->total = (int)max($total, 0); + + return $this; + } + + /** + * @param Route $route + * @param int $total + * @param int|null $pos + * @param int|null $limit + * @param array|null $options + * @return void + */ + protected function initialize(Route $route, int $total, int $pos = null, int $limit = null, array $options = null) + { + $this->setRoute($route); + $this->setOptions($options); + $this->setTotal($total); + if ($this->getOptions()['type'] === 'start') { + $this->setStart($pos); + } else { + $this->setPage($pos); + } + $this->setLimit($limit); + $this->calculateLimits(); + } + + /** + * @return void + */ + protected function calculateLimits() + { + $limit = $this->limit; + $total = $this->total; + + if (!$limit || $limit > $total) { + // All records fit into a single page. + $this->start = 0; + $this->page = 1; + $this->pages = 1; + + return; + } + + if (null === $this->start) { + // If we are using page, convert it to start. + $this->start = (int)(($this->page - 1) * $limit); + } + + if ($this->start > $total - $limit) { + // If start is greater than total count (i.e. we are asked to display records that don't exist) + // then set start to display the last natural page of results. + $this->start = (int)max(0, (ceil($total / $limit) - 1) * $limit); + } + + // Set the total pages and current page values. + $this->page = (int)ceil(($this->start + 1) / $limit); + $this->pages = (int)ceil($total / $limit); + } + + /** + * @return void + */ + protected function calculateRange() + { + $options = $this->getOptions(); + $displayed = $options['display']; + $opening = $options['opening']; + $ending = $options['ending']; + + // Set the pagination iteration loop values. + $this->pagesStart = $this->page - (int)($displayed / 2); + if ($this->pagesStart < 1 + $opening) { + $this->pagesStart = 1 + $opening; + } + if ($this->pagesStart + $displayed - $opening > $this->pages) { + $this->pagesStop = $this->pages; + if ($this->pages < $displayed) { + $this->pagesStart = 1 + $opening; + } else { + $this->pagesStart = $this->pages - $displayed + 1 + $opening; + } + } else { + $this->pagesStop = (int)max(1, $this->pagesStart + $displayed - 1 - $ending); + } + } +} diff --git a/system/src/Grav/Framework/Pagination/AbstractPaginationPage.php b/system/src/Grav/Framework/Pagination/AbstractPaginationPage.php new file mode 100644 index 0000000..95ff8af --- /dev/null +++ b/system/src/Grav/Framework/Pagination/AbstractPaginationPage.php @@ -0,0 +1,78 @@ +options['active'] ?? false; + } + + /** + * @return bool + */ + public function isEnabled(): bool + { + return $this->options['enabled'] ?? false; + } + + /** + * @return array + */ + public function getOptions(): array + { + return $this->options ?? []; + } + + /** + * @return int|null + */ + public function getNumber(): ?int + { + return $this->options['number'] ?? null; + } + + /** + * @return string + */ + public function getLabel(): string + { + return $this->options['label'] ?? (string)$this->getNumber(); + } + + /** + * @return string|null + */ + public function getUrl(): ?string + { + return $this->options['route'] ? (string)$this->options['route']->getUri() : null; + } + + /** + * @param array $options + */ + protected function setOptions(array $options): void + { + $this->options = $options; + } +} diff --git a/system/src/Grav/Framework/Pagination/Interfaces/PaginationInterface.php b/system/src/Grav/Framework/Pagination/Interfaces/PaginationInterface.php new file mode 100644 index 0000000..56d3361 --- /dev/null +++ b/system/src/Grav/Framework/Pagination/Interfaces/PaginationInterface.php @@ -0,0 +1,103 @@ +initialize($route, $total, $pos, $limit, $options); + } +} diff --git a/system/src/Grav/Framework/Pagination/PaginationPage.php b/system/src/Grav/Framework/Pagination/PaginationPage.php new file mode 100644 index 0000000..63ceb5f --- /dev/null +++ b/system/src/Grav/Framework/Pagination/PaginationPage.php @@ -0,0 +1,26 @@ +setOptions($options); + } +} diff --git a/system/src/Grav/Framework/Psr7/AbstractUri.php b/system/src/Grav/Framework/Psr7/AbstractUri.php new file mode 100644 index 0000000..85d802f --- /dev/null +++ b/system/src/Grav/Framework/Psr7/AbstractUri.php @@ -0,0 +1,411 @@ + 80, + 'https' => 443 + ]; + + /** @var string Uri scheme. */ + private $scheme = ''; + /** @var string Uri user. */ + private $user = ''; + /** @var string Uri password. */ + private $password = ''; + /** @var string Uri host. */ + private $host = ''; + /** @var int|null Uri port. */ + private $port; + /** @var string Uri path. */ + private $path = ''; + /** @var string Uri query string (without ?). */ + private $query = ''; + /** @var string Uri fragment (without #). */ + private $fragment = ''; + + /** + * Please define constructor which calls $this->init(). + */ + abstract public function __construct(); + + /** + * @inheritdoc + */ + public function getScheme() + { + return $this->scheme; + } + + /** + * @inheritdoc + */ + public function getAuthority() + { + $authority = $this->host; + + $userInfo = $this->getUserInfo(); + if ($userInfo !== '') { + $authority = $userInfo . '@' . $authority; + } + + if ($this->port !== null) { + $authority .= ':' . $this->port; + } + + return $authority; + } + + /** + * @inheritdoc + */ + public function getUserInfo() + { + $userInfo = $this->user; + + if ($this->password !== '') { + $userInfo .= ':' . $this->password; + } + + return $userInfo; + } + + /** + * @inheritdoc + */ + public function getHost() + { + return $this->host; + } + + /** + * @inheritdoc + */ + public function getPort() + { + return $this->port; + } + + /** + * @inheritdoc + */ + public function getPath() + { + return $this->path; + } + + /** + * @inheritdoc + */ + public function getQuery() + { + return $this->query; + } + + /** + * @inheritdoc + */ + public function getFragment() + { + return $this->fragment; + } + + /** + * @inheritdoc + */ + public function withScheme($scheme) + { + $scheme = UriPartsFilter::filterScheme($scheme); + + if ($this->scheme === $scheme) { + return $this; + } + + $new = clone $this; + $new->scheme = $scheme; + $new->unsetDefaultPort(); + $new->validate(); + + return $new; + } + + /** + * @inheritdoc + * @throws InvalidArgumentException + */ + public function withUserInfo($user, $password = null) + { + $user = UriPartsFilter::filterUserInfo($user); + $password = UriPartsFilter::filterUserInfo($password ?? ''); + + if ($this->user === $user && $this->password === $password) { + return $this; + } + + $new = clone $this; + $new->user = $user; + $new->password = $user !== '' ? $password : ''; + $new->validate(); + + return $new; + } + + /** + * @inheritdoc + */ + public function withHost($host) + { + $host = UriPartsFilter::filterHost($host); + + if ($this->host === $host) { + return $this; + } + + $new = clone $this; + $new->host = $host; + $new->validate(); + + return $new; + } + + /** + * @inheritdoc + */ + public function withPort($port) + { + $port = UriPartsFilter::filterPort($port); + + if ($this->port === $port) { + return $this; + } + + $new = clone $this; + $new->port = $port; + $new->unsetDefaultPort(); + $new->validate(); + + return $new; + } + + /** + * @inheritdoc + */ + public function withPath($path) + { + $path = UriPartsFilter::filterPath($path); + + if ($this->path === $path) { + return $this; + } + + $new = clone $this; + $new->path = $path; + $new->validate(); + + return $new; + } + + /** + * @inheritdoc + */ + public function withQuery($query) + { + $query = UriPartsFilter::filterQueryOrFragment($query); + + if ($this->query === $query) { + return $this; + } + + $new = clone $this; + $new->query = $query; + + return $new; + } + + /** + * @inheritdoc + * @throws InvalidArgumentException + */ + public function withFragment($fragment) + { + $fragment = UriPartsFilter::filterQueryOrFragment($fragment); + + if ($this->fragment === $fragment) { + return $this; + } + + $new = clone $this; + $new->fragment = $fragment; + + return $new; + } + + /** + * @return string + */ + public function __toString() + { + return $this->getUrl(); + } + + /** + * @return array + */ + protected function getParts() + { + return [ + 'scheme' => $this->scheme, + 'host' => $this->host, + 'port' => $this->port, + 'user' => $this->user, + 'pass' => $this->password, + 'path' => $this->path, + 'query' => $this->query, + 'fragment' => $this->fragment + ]; + } + + /** + * Return the fully qualified base URL ( like http://getgrav.org ). + * + * Note that this method never includes a trailing / + * + * @return string + */ + protected function getBaseUrl() + { + $uri = ''; + + $scheme = $this->getScheme(); + if ($scheme !== '') { + $uri .= $scheme . ':'; + } + + $authority = $this->getAuthority(); + if ($authority !== '' || $scheme === 'file') { + $uri .= '//' . $authority; + } + + return $uri; + } + + /** + * @return string + */ + protected function getUrl() + { + $uri = $this->getBaseUrl() . $this->getPath(); + + $query = $this->getQuery(); + if ($query !== '') { + $uri .= '?' . $query; + } + + $fragment = $this->getFragment(); + if ($fragment !== '') { + $uri .= '#' . $fragment; + } + + return $uri; + } + + /** + * @return string + */ + protected function getUser() + { + return $this->user; + } + + /** + * @return string + */ + protected function getPassword() + { + return $this->password; + } + + /** + * @param array $parts + * @return void + * @throws InvalidArgumentException + */ + protected function initParts(array $parts) + { + $this->scheme = isset($parts['scheme']) ? UriPartsFilter::filterScheme($parts['scheme']) : ''; + $this->user = isset($parts['user']) ? UriPartsFilter::filterUserInfo($parts['user']) : ''; + $this->password = isset($parts['pass']) ? UriPartsFilter::filterUserInfo($parts['pass']) : ''; + $this->host = isset($parts['host']) ? UriPartsFilter::filterHost($parts['host']) : ''; + $this->port = isset($parts['port']) ? UriPartsFilter::filterPort((int)$parts['port']) : null; + $this->path = isset($parts['path']) ? UriPartsFilter::filterPath($parts['path']) : ''; + $this->query = isset($parts['query']) ? UriPartsFilter::filterQueryOrFragment($parts['query']) : ''; + $this->fragment = isset($parts['fragment']) ? UriPartsFilter::filterQueryOrFragment($parts['fragment']) : ''; + + $this->unsetDefaultPort(); + $this->validate(); + } + + /** + * @return void + * @throws InvalidArgumentException + */ + private function validate() + { + if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) { + throw new InvalidArgumentException('Uri with a scheme must have a host'); + } + + if ($this->getAuthority() === '') { + if (0 === strpos($this->path, '//')) { + throw new InvalidArgumentException('The path of a URI without an authority must not start with two slashes \'//\''); + } + if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) { + throw new InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon'); + } + } elseif (isset($this->path[0]) && $this->path[0] !== '/') { + throw new InvalidArgumentException('The path of a URI with an authority must start with a slash \'/\' or be empty'); + } + } + + /** + * @return bool + */ + protected function isDefaultPort() + { + $scheme = $this->scheme; + $port = $this->port; + + return $this->port === null + || (isset(static::$defaultPorts[$scheme]) && $port === static::$defaultPorts[$scheme]); + } + + /** + * @return void + */ + private function unsetDefaultPort() + { + if ($this->isDefaultPort()) { + $this->port = null; + } + } +} diff --git a/system/src/Grav/Framework/Psr7/Request.php b/system/src/Grav/Framework/Psr7/Request.php new file mode 100644 index 0000000..cd65fd9 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Request.php @@ -0,0 +1,34 @@ +message = new \Nyholm\Psr7\Request($method, $uri, $headers, $body, $version); + } +} diff --git a/system/src/Grav/Framework/Psr7/Response.php b/system/src/Grav/Framework/Psr7/Response.php new file mode 100644 index 0000000..6cb67e0 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Response.php @@ -0,0 +1,264 @@ +message = new \Nyholm\Psr7\Response($status, $headers, $body, $version, $reason); + } + + /** + * Json. + * + * Note: This method is not part of the PSR-7 standard. + * + * This method prepares the response object to return an HTTP Json + * response to the client. + * + * @param mixed $data The data + * @param int|null $status The HTTP status code. + * @param int $options Json encoding options + * @param int $depth Json encoding max depth + * @return static + */ + public function withJson($data, int $status = null, int $options = 0, int $depth = 512): ResponseInterface + { + $json = (string) json_encode($data, $options, $depth); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException(json_last_error_msg(), json_last_error()); + } + + $response = $this->getResponse() + ->withHeader('Content-Type', 'application/json;charset=utf-8') + ->withBody(new Stream($json)); + + if ($status !== null) { + $response = $response->withStatus($status); + } + + $new = clone $this; + $new->message = $response; + + return $new; + } + + /** + * Redirect. + * + * Note: This method is not part of the PSR-7 standard. + * + * This method prepares the response object to return an HTTP Redirect + * response to the client. + * + * @param string $url The redirect destination. + * @param int|null $status The redirect HTTP status code. + * @return static + */ + public function withRedirect(string $url, $status = null): ResponseInterface + { + $response = $this->getResponse()->withHeader('Location', $url); + + if ($status === null) { + $status = 302; + } + + $new = clone $this; + $new->message = $response->withStatus($status); + + return $new; + } + + /** + * Is this response empty? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isEmpty(): bool + { + return in_array($this->getResponse()->getStatusCode(), [204, 205, 304], true); + } + + + /** + * Is this response OK? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isOk(): bool + { + return $this->getResponse()->getStatusCode() === 200; + } + + /** + * Is this response a redirect? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isRedirect(): bool + { + return in_array($this->getResponse()->getStatusCode(), [301, 302, 303, 307, 308], true); + } + + /** + * Is this response forbidden? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + * @api + */ + public function isForbidden(): bool + { + return $this->getResponse()->getStatusCode() === 403; + } + + /** + * Is this response not Found? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isNotFound(): bool + { + return $this->getResponse()->getStatusCode() === 404; + } + + /** + * Is this response informational? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isInformational(): bool + { + $response = $this->getResponse(); + + return $response->getStatusCode() >= 100 && $response->getStatusCode() < 200; + } + + /** + * Is this response successful? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isSuccessful(): bool + { + $response = $this->getResponse(); + + return $response->getStatusCode() >= 200 && $response->getStatusCode() < 300; + } + + /** + * Is this response a redirection? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isRedirection(): bool + { + $response = $this->getResponse(); + + return $response->getStatusCode() >= 300 && $response->getStatusCode() < 400; + } + + /** + * Is this response a client error? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isClientError(): bool + { + $response = $this->getResponse(); + + return $response->getStatusCode() >= 400 && $response->getStatusCode() < 500; + } + + /** + * Is this response a server error? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isServerError(): bool + { + $response = $this->getResponse(); + + return $response->getStatusCode() >= 500 && $response->getStatusCode() < 600; + } + + /** + * Convert response to string. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string + */ + public function __toString(): string + { + $response = $this->getResponse(); + $output = sprintf( + 'HTTP/%s %s %s%s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase(), + self::EOL + ); + + foreach ($response->getHeaders() as $name => $values) { + $output .= sprintf('%s: %s', $name, $response->getHeaderLine($name)) . self::EOL; + } + + $output .= self::EOL; + $output .= (string) $response->getBody(); + + return $output; + } +} diff --git a/system/src/Grav/Framework/Psr7/ServerRequest.php b/system/src/Grav/Framework/Psr7/ServerRequest.php new file mode 100644 index 0000000..4084834 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/ServerRequest.php @@ -0,0 +1,374 @@ +message = new \Nyholm\Psr7\ServerRequest($method, $uri, $headers, $body, $version, $serverParams); + } + + /** + * Get serverRequest content character set, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string|null + */ + public function getContentCharset(): ?string + { + $mediaTypeParams = $this->getMediaTypeParams(); + + if (isset($mediaTypeParams['charset'])) { + return $mediaTypeParams['charset']; + } + + return null; + } + + /** + * Get serverRequest content type. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string|null The serverRequest content type, if known + */ + public function getContentType(): ?string + { + $result = $this->getRequest()->getHeader('Content-Type'); + + return $result ? $result[0] : null; + } + + /** + * Get serverRequest content length, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return int|null + */ + public function getContentLength(): ?int + { + $result = $this->getRequest()->getHeader('Content-Length'); + + return $result ? (int) $result[0] : null; + } + + /** + * Fetch cookie value from cookies sent by the client to the server. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * + * @return mixed + */ + public function getCookieParam($key, $default = null) + { + $cookies = $this->getRequest()->getCookieParams(); + $result = $default; + + if (isset($cookies[$key])) { + $result = $cookies[$key]; + } + + return $result; + } + + /** + * Get serverRequest media type, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string|null The serverRequest media type, minus content-type params + */ + public function getMediaType(): ?string + { + $contentType = $this->getContentType(); + + if ($contentType) { + $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType); + if ($contentTypeParts === false) { + return null; + } + return strtolower($contentTypeParts[0]); + } + + return null; + } + + /** + * Get serverRequest media type params, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return mixed[] + */ + public function getMediaTypeParams(): array + { + $contentType = $this->getContentType(); + $contentTypeParams = []; + + if ($contentType) { + $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType); + if ($contentTypeParts !== false) { + $contentTypePartsLength = count($contentTypeParts); + for ($i = 1; $i < $contentTypePartsLength; $i++) { + $paramParts = explode('=', $contentTypeParts[$i]); + $contentTypeParams[strtolower($paramParts[0])] = $paramParts[1]; + } + } + } + + return $contentTypeParams; + } + + /** + * Fetch serverRequest parameter value from body or query string (in that order). + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key The parameter key. + * @param string|null $default The default value. + * + * @return mixed The parameter value. + */ + public function getParam($key, $default = null) + { + $postParams = $this->getParsedBody(); + $getParams = $this->getQueryParams(); + $result = $default; + + if (is_array($postParams) && isset($postParams[$key])) { + $result = $postParams[$key]; + } elseif (is_object($postParams) && property_exists($postParams, $key)) { + $result = $postParams->$key; + } elseif (isset($getParams[$key])) { + $result = $getParams[$key]; + } + + return $result; + } + + /** + * Fetch associative array of body and query string parameters. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return mixed[] + */ + public function getParams(): array + { + $params = $this->getQueryParams(); + $postParams = $this->getParsedBody(); + + if ($postParams) { + $params = array_merge($params, (array)$postParams); + } + + return $params; + } + + /** + * Fetch parameter value from serverRequest body. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function getParsedBodyParam($key, $default = null) + { + $postParams = $this->getParsedBody(); + $result = $default; + + if (is_array($postParams) && isset($postParams[$key])) { + $result = $postParams[$key]; + } elseif (is_object($postParams) && property_exists($postParams, $key)) { + $result = $postParams->{$key}; + } + + return $result; + } + + /** + * Fetch parameter value from query string. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function getQueryParam($key, $default = null) + { + $getParams = $this->getQueryParams(); + $result = $default; + + if (isset($getParams[$key])) { + $result = $getParams[$key]; + } + + return $result; + } + + /** + * Retrieve a server parameter. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function getServerParam($key, $default = null) + { + $serverParams = $this->getRequest()->getServerParams(); + + return $serverParams[$key] ?? $default; + } + + /** + * Does this serverRequest use a given method? + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $method HTTP method + * @return bool + */ + public function isMethod($method): bool + { + return $this->getRequest()->getMethod() === $method; + } + + /** + * Is this a DELETE serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isDelete(): bool + { + return $this->isMethod('DELETE'); + } + + /** + * Is this a GET serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isGet(): bool + { + return $this->isMethod('GET'); + } + + /** + * Is this a HEAD serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isHead(): bool + { + return $this->isMethod('HEAD'); + } + + /** + * Is this a OPTIONS serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isOptions(): bool + { + return $this->isMethod('OPTIONS'); + } + + /** + * Is this a PATCH serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isPatch(): bool + { + return $this->isMethod('PATCH'); + } + + /** + * Is this a POST serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isPost(): bool + { + return $this->isMethod('POST'); + } + + /** + * Is this a PUT serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isPut(): bool + { + return $this->isMethod('PUT'); + } + + /** + * Is this an XHR serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isXhr(): bool + { + return $this->getRequest()->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; + } +} diff --git a/system/src/Grav/Framework/Psr7/Stream.php b/system/src/Grav/Framework/Psr7/Stream.php new file mode 100644 index 0000000..a5a1043 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Stream.php @@ -0,0 +1,43 @@ +stream = \Nyholm\Psr7\Stream::create($body); + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/MessageDecoratorTrait.php b/system/src/Grav/Framework/Psr7/Traits/MessageDecoratorTrait.php new file mode 100644 index 0000000..39e3818 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/MessageDecoratorTrait.php @@ -0,0 +1,140 @@ + + */ +trait MessageDecoratorTrait +{ + /** @var MessageInterface */ + private $message; + + /** + * Returns the decorated message. + * + * Since the underlying Message is immutable as well + * exposing it is not an issue, because it's state cannot be altered + * + * @return MessageInterface + */ + public function getMessage(): MessageInterface + { + return $this->message; + } + + /** + * {@inheritdoc} + */ + public function getProtocolVersion(): string + { + return $this->message->getProtocolVersion(); + } + + /** + * {@inheritdoc} + */ + public function withProtocolVersion($version): self + { + $new = clone $this; + $new->message = $this->message->withProtocolVersion($version); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getHeaders(): array + { + return $this->message->getHeaders(); + } + + /** + * {@inheritdoc} + */ + public function hasHeader($header): bool + { + return $this->message->hasHeader($header); + } + + /** + * {@inheritdoc} + */ + public function getHeader($header): array + { + return $this->message->getHeader($header); + } + + /** + * {@inheritdoc} + */ + public function getHeaderLine($header): string + { + return $this->message->getHeaderLine($header); + } + + /** + * {@inheritdoc} + */ + public function getBody(): StreamInterface + { + return $this->message->getBody(); + } + + /** + * {@inheritdoc} + */ + public function withHeader($header, $value): self + { + $new = clone $this; + $new->message = $this->message->withHeader($header, $value); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function withAddedHeader($header, $value): self + { + $new = clone $this; + $new->message = $this->message->withAddedHeader($header, $value); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function withoutHeader($header): self + { + $new = clone $this; + $new->message = $this->message->withoutHeader($header); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function withBody(StreamInterface $body): self + { + $new = clone $this; + $new->message = $this->message->withBody($body); + + return $new; + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/RequestDecoratorTrait.php b/system/src/Grav/Framework/Psr7/Traits/RequestDecoratorTrait.php new file mode 100644 index 0000000..13f4c6f --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/RequestDecoratorTrait.php @@ -0,0 +1,112 @@ + + */ +trait RequestDecoratorTrait +{ + use MessageDecoratorTrait { + getMessage as private; + } + + /** + * Returns the decorated request. + * + * Since the underlying Request is immutable as well + * exposing it is not an issue, because it's state cannot be altered + * + * @return RequestInterface + */ + public function getRequest(): RequestInterface + { + /** @var RequestInterface $message */ + $message = $this->getMessage(); + + return $message; + } + + /** + * Exchanges the underlying request with another. + * + * @param RequestInterface $request + * @return self + */ + public function withRequest(RequestInterface $request): self + { + $new = clone $this; + $new->message = $request; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getRequestTarget(): string + { + return $this->getRequest()->getRequestTarget(); + } + + /** + * {@inheritdoc} + */ + public function withRequestTarget($requestTarget): self + { + $new = clone $this; + $new->message = $this->getRequest()->withRequestTarget($requestTarget); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getMethod(): string + { + return $this->getRequest()->getMethod(); + } + + /** + * {@inheritdoc} + */ + public function withMethod($method): self + { + $new = clone $this; + $new->message = $this->getRequest()->withMethod($method); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getUri(): UriInterface + { + return $this->getRequest()->getUri(); + } + + /** + * {@inheritdoc} + */ + public function withUri(UriInterface $uri, $preserveHost = false): self + { + $new = clone $this; + $new->message = $this->getRequest()->withUri($uri, $preserveHost); + + return $new; + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/ResponseDecoratorTrait.php b/system/src/Grav/Framework/Psr7/Traits/ResponseDecoratorTrait.php new file mode 100644 index 0000000..7b62c3c --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/ResponseDecoratorTrait.php @@ -0,0 +1,82 @@ + + */ +trait ResponseDecoratorTrait +{ + use MessageDecoratorTrait { + getMessage as private; + } + + /** + * Returns the decorated response. + * + * Since the underlying Response is immutable as well + * exposing it is not an issue, because it's state cannot be altered + * + * @return ResponseInterface + */ + public function getResponse(): ResponseInterface + { + /** @var ResponseInterface $message */ + $message = $this->getMessage(); + + return $message; + } + + /** + * Exchanges the underlying response with another. + * + * @param ResponseInterface $response + * + * @return self + */ + public function withResponse(ResponseInterface $response): self + { + $new = clone $this; + $new->message = $response; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getStatusCode(): int + { + return $this->getResponse()->getStatusCode(); + } + + /** + * {@inheritdoc} + */ + public function withStatus($code, $reasonPhrase = ''): self + { + $new = clone $this; + $new->message = $this->getResponse()->withStatus($code, $reasonPhrase); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getReasonPhrase(): string + { + return $this->getResponse()->getReasonPhrase(); + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/ServerRequestDecoratorTrait.php b/system/src/Grav/Framework/Psr7/Traits/ServerRequestDecoratorTrait.php new file mode 100644 index 0000000..33c26e0 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/ServerRequestDecoratorTrait.php @@ -0,0 +1,176 @@ +getMessage(); + + return $message; + } + + /** + * @inheritdoc + */ + public function getAttribute($name, $default = null) + { + return $this->getRequest()->getAttribute($name, $default); + } + + /** + * @inheritdoc + */ + public function getAttributes() + { + return $this->getRequest()->getAttributes(); + } + + + /** + * @inheritdoc + */ + public function getCookieParams() + { + return $this->getRequest()->getCookieParams(); + } + + /** + * @inheritdoc + */ + public function getParsedBody() + { + return $this->getRequest()->getParsedBody(); + } + + /** + * @inheritdoc + */ + public function getQueryParams() + { + return $this->getRequest()->getQueryParams(); + } + + /** + * @inheritdoc + */ + public function getServerParams() + { + return $this->getRequest()->getServerParams(); + } + + /** + * @inheritdoc + */ + public function getUploadedFiles() + { + return $this->getRequest()->getUploadedFiles(); + } + + /** + * @inheritdoc + */ + public function withAttribute($name, $value) + { + $new = clone $this; + $new->message = $this->getRequest()->withAttribute($name, $value); + + return $new; + } + + /** + * @param array $attributes + * @return ServerRequestInterface + */ + public function withAttributes(array $attributes) + { + $new = clone $this; + foreach ($attributes as $attribute => $value) { + $new->message = $new->withAttribute($attribute, $value); + } + + return $new; + } + + /** + * @inheritdoc + */ + public function withoutAttribute($name) + { + $new = clone $this; + $new->message = $this->getRequest()->withoutAttribute($name); + + return $new; + } + + /** + * @inheritdoc + */ + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->message = $this->getRequest()->withCookieParams($cookies); + + return $new; + } + + /** + * @inheritdoc + */ + public function withParsedBody($data) + { + $new = clone $this; + $new->message = $this->getRequest()->withParsedBody($data); + + return $new; + } + + /** + * @inheritdoc + */ + public function withQueryParams(array $query) + { + $new = clone $this; + $new->message = $this->getRequest()->withQueryParams($query); + + return $new; + } + + /** + * @inheritdoc + */ + public function withUploadedFiles(array $uploadedFiles) + { + $new = clone $this; + $new->message = $this->getRequest()->withUploadedFiles($uploadedFiles); + + return $new; + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/StreamDecoratorTrait.php b/system/src/Grav/Framework/Psr7/Traits/StreamDecoratorTrait.php new file mode 100644 index 0000000..db1e746 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/StreamDecoratorTrait.php @@ -0,0 +1,152 @@ +stream->__toString(); + } + + /** + * @return void + */ + public function __destruct() + { + $this->stream->close(); + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + $this->stream->close(); + } + + /** + * {@inheritdoc} + */ + public function detach() + { + return $this->stream->detach(); + } + + /** + * {@inheritdoc} + */ + public function getSize(): ?int + { + return $this->stream->getSize(); + } + + /** + * {@inheritdoc} + */ + public function tell(): int + { + return $this->stream->tell(); + } + + /** + * {@inheritdoc} + */ + public function eof(): bool + { + return $this->stream->eof(); + } + + /** + * {@inheritdoc} + */ + public function isSeekable(): bool + { + return $this->stream->isSeekable(); + } + + /** + * {@inheritdoc} + */ + public function seek($offset, $whence = \SEEK_SET): void + { + $this->stream->seek($offset, $whence); + } + + /** + * {@inheritdoc} + */ + public function rewind(): void + { + $this->stream->rewind(); + } + + /** + * {@inheritdoc} + */ + public function isWritable(): bool + { + return $this->stream->isWritable(); + } + + /** + * {@inheritdoc} + */ + public function write($string): int + { + return $this->stream->write($string); + } + + /** + * {@inheritdoc} + */ + public function isReadable(): bool + { + return $this->stream->isReadable(); + } + + /** + * {@inheritdoc} + */ + public function read($length): string + { + return $this->stream->read($length); + } + + /** + * {@inheritdoc} + */ + public function getContents(): string + { + return $this->stream->getContents(); + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key = null) + { + return $this->stream->getMetadata($key); + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/UploadedFileDecoratorTrait.php b/system/src/Grav/Framework/Psr7/Traits/UploadedFileDecoratorTrait.php new file mode 100644 index 0000000..e0c65bd --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/UploadedFileDecoratorTrait.php @@ -0,0 +1,73 @@ +uploadedFile->getStream(); + } + + /** + * @param string $targetPath + */ + public function moveTo($targetPath): void + { + $this->uploadedFile->moveTo($targetPath); + } + + /** + * @return int|null + */ + public function getSize(): ?int + { + return $this->uploadedFile->getSize(); + } + + /** + * @return int + */ + public function getError(): int + { + return $this->uploadedFile->getError(); + } + + /** + * @return string|null + */ + public function getClientFilename(): ?string + { + return $this->uploadedFile->getClientFilename(); + } + + /** + * @return string|null + */ + public function getClientMediaType(): ?string + { + return $this->uploadedFile->getClientMediaType(); + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/UriDecorationTrait.php b/system/src/Grav/Framework/Psr7/Traits/UriDecorationTrait.php new file mode 100644 index 0000000..607d757 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/UriDecorationTrait.php @@ -0,0 +1,188 @@ +uri->__toString(); + } + + /** + * @return string + */ + public function getScheme(): string + { + return $this->uri->getScheme(); + } + + /** + * @return string + */ + public function getAuthority(): string + { + return $this->uri->getAuthority(); + } + + /** + * @return string + */ + public function getUserInfo(): string + { + return $this->uri->getUserInfo(); + } + + /** + * @return string + */ + public function getHost(): string + { + return $this->uri->getHost(); + } + + /** + * @return int|null + */ + public function getPort(): ?int + { + return $this->uri->getPort(); + } + + /** + * @return string + */ + public function getPath(): string + { + return $this->uri->getPath(); + } + + /** + * @return string + */ + public function getQuery(): string + { + return $this->uri->getQuery(); + } + + /** + * @return string + */ + public function getFragment(): string + { + return $this->uri->getFragment(); + } + + /** + * @param string $scheme + * @return UriInterface + */ + public function withScheme($scheme): UriInterface + { + $new = clone $this; + $new->uri = $this->uri->withScheme($scheme); + + /** @var UriInterface $new */ + return $new; + } + + /** + * @param string $user + * @param string|null $password + * @return UriInterface + */ + public function withUserInfo($user, $password = null): UriInterface + { + $new = clone $this; + $new->uri = $this->uri->withUserInfo($user, $password); + + /** @var UriInterface $new */ + return $new; + } + + /** + * @param string $host + * @return UriInterface + */ + public function withHost($host): UriInterface + { + $new = clone $this; + $new->uri = $this->uri->withHost($host); + + /** @var UriInterface $new */ + return $new; + } + + /** + * @param int|null $port + * @return UriInterface + */ + public function withPort($port): UriInterface + { + $new = clone $this; + $new->uri = $this->uri->withPort($port); + + /** @var UriInterface $new */ + return $new; + } + + /** + * @param string $path + * @return UriInterface + */ + public function withPath($path): UriInterface + { + $new = clone $this; + $new->uri = $this->uri->withPath($path); + + /** @var UriInterface $new */ + return $new; + } + + /** + * @param string $query + * @return UriInterface + */ + public function withQuery($query): UriInterface + { + $new = clone $this; + $new->uri = $this->uri->withQuery($query); + + /** @var UriInterface $new */ + return $new; + } + + /** + * @param string $fragment + * @return UriInterface + */ + public function withFragment($fragment): UriInterface + { + $new = clone $this; + $new->uri = $this->uri->withFragment($fragment); + + /** @var UriInterface $new */ + return $new; + } +} diff --git a/system/src/Grav/Framework/Psr7/UploadedFile.php b/system/src/Grav/Framework/Psr7/UploadedFile.php new file mode 100644 index 0000000..bfa63cd --- /dev/null +++ b/system/src/Grav/Framework/Psr7/UploadedFile.php @@ -0,0 +1,70 @@ +uploadedFile = new \Nyholm\Psr7\UploadedFile($streamOrFile, $size, $errorStatus, $clientFilename, $clientMediaType); + } + + /** + * @param array $meta + * @return $this + */ + public function setMeta(array $meta) + { + $this->meta = $meta; + + return $this; + } + + /** + * @param array $meta + * @return $this + */ + public function addMeta(array $meta) + { + $this->meta = array_merge($this->meta, $meta); + + return $this; + } + + /** + * @return array + */ + public function getMeta(): array + { + return $this->meta; + } +} diff --git a/system/src/Grav/Framework/Psr7/Uri.php b/system/src/Grav/Framework/Psr7/Uri.php new file mode 100644 index 0000000..c920b33 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Uri.php @@ -0,0 +1,138 @@ +uri = new \Nyholm\Psr7\Uri($uri); + } + + /** + * @return array + */ + public function getQueryParams(): array + { + return UriFactory::parseQuery($this->getQuery()); + } + + /** + * @param array $params + * @return UriInterface + */ + public function withQueryParams(array $params): UriInterface + { + $query = UriFactory::buildQuery($params); + + return $this->withQuery($query); + } + + /** + * Whether the URI has the default port of the current scheme. + * + * `$uri->getPort()` may return the standard port. This method can be used for some non-http/https Uri. + * + * @return bool + */ + public function isDefaultPort(): bool + { + return $this->getPort() === null || GuzzleUri::isDefaultPort($this); + } + + /** + * Whether the URI is absolute, i.e. it has a scheme. + * + * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true + * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative + * to another URI, the base URI. Relative references can be divided into several forms: + * - network-path references, e.g. '//example.com/path' + * - absolute-path references, e.g. '/path' + * - relative-path references, e.g. 'subpath' + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4 + */ + public function isAbsolute(): bool + { + return GuzzleUri::isAbsolute($this); + } + + /** + * Whether the URI is a network-path reference. + * + * A relative reference that begins with two slash characters is termed an network-path reference. + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public function isNetworkPathReference(): bool + { + return GuzzleUri::isNetworkPathReference($this); + } + + /** + * Whether the URI is a absolute-path reference. + * + * A relative reference that begins with a single slash character is termed an absolute-path reference. + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public function isAbsolutePathReference(): bool + { + return GuzzleUri::isAbsolutePathReference($this); + } + + /** + * Whether the URI is a relative-path reference. + * + * A relative reference that does not begin with a slash character is termed a relative-path reference. + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public function isRelativePathReference(): bool + { + return GuzzleUri::isRelativePathReference($this); + } + + /** + * Whether the URI is a same-document reference. + * + * A same-document reference refers to a URI that is, aside from its fragment + * component, identical to the base URI. When no base URI is given, only an empty + * URI reference (apart from its fragment) is considered a same-document reference. + * + * @param UriInterface|null $base An optional base URI to compare against + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.4 + */ + public function isSameDocumentReference(UriInterface $base = null): bool + { + return GuzzleUri::isSameDocumentReference($this, $base); + } +} diff --git a/system/src/Grav/Framework/RequestHandler/Exception/InvalidArgumentException.php b/system/src/Grav/Framework/RequestHandler/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..451e586 --- /dev/null +++ b/system/src/Grav/Framework/RequestHandler/Exception/InvalidArgumentException.php @@ -0,0 +1,49 @@ +invalidMiddleware = $invalidMiddleware; + } + + /** + * Return the invalid middleware + * + * @return mixed|null + */ + public function getInvalidMiddleware() + { + return $this->invalidMiddleware; + } +} diff --git a/system/src/Grav/Framework/RequestHandler/Exception/NotFoundException.php b/system/src/Grav/Framework/RequestHandler/Exception/NotFoundException.php new file mode 100644 index 0000000..28a226a --- /dev/null +++ b/system/src/Grav/Framework/RequestHandler/Exception/NotFoundException.php @@ -0,0 +1,45 @@ +getMethod()), ['PUT', 'PATCH', 'DELETE'])) { + parent::__construct($request, 'Method Not Allowed', 405, $previous); + } else { + parent::__construct($request, 'Not Found', 404, $previous); + } + } + + public function getRequest(): ServerRequestInterface + { + return $this->request; + } +} diff --git a/system/src/Grav/Framework/RequestHandler/Exception/NotHandledException.php b/system/src/Grav/Framework/RequestHandler/Exception/NotHandledException.php new file mode 100644 index 0000000..1b18473 --- /dev/null +++ b/system/src/Grav/Framework/RequestHandler/Exception/NotHandledException.php @@ -0,0 +1,20 @@ + 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Requested range not satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 419 => 'Page Expired', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 511 => 'Network Authentication Required', + ]; + + /** @var ServerRequestInterface */ + private $request; + + /** + * @param ServerRequestInterface $request + * @param string $message + * @param int $code + * @param Throwable|null $previous + */ + public function __construct(ServerRequestInterface $request, string $message, int $code = 500, Throwable $previous = null) + { + $this->request = $request; + + parent::__construct($message, $code, $previous); + } + + /** + * @return ServerRequestInterface + */ + public function getRequest(): ServerRequestInterface + { + return $this->request; + } + + public function getHttpCode(): int + { + $code = $this->getCode(); + + return isset(self::$phrases[$code]) ? $code : 500; + } + + public function getHttpReason(): ?string + { + return self::$phrases[$this->getCode()] ?? self::$phrases[500]; + } +} diff --git a/system/src/Grav/Framework/RequestHandler/Middlewares/Exceptions.php b/system/src/Grav/Framework/RequestHandler/Middlewares/Exceptions.php new file mode 100644 index 0000000..9eadf72 --- /dev/null +++ b/system/src/Grav/Framework/RequestHandler/Middlewares/Exceptions.php @@ -0,0 +1,80 @@ +handle($request); + } catch (Throwable $exception) { + $code = $exception->getCode(); + if ($exception instanceof ValidationException) { + $message = $exception->getMessage(); + } else { + $message = htmlspecialchars($exception->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } + + $extra = $exception instanceof JsonSerializable ? $exception->jsonSerialize() : []; + + $response = [ + 'code' => $code, + 'status' => 'error', + 'message' => $message, + 'error' => [ + 'code' => $code, + 'message' => $message, + ] + $extra + ]; + + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + if ($debugger->enabled()) { + $response['error'] += [ + 'type' => get_class($exception), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => explode("\n", $exception->getTraceAsString()), + ]; + } + + /** @var string $json */ + $json = json_encode($response, JSON_THROW_ON_ERROR); + + return new Response($code ?: 500, ['Content-Type' => 'application/json'], $json); + } + } +} diff --git a/system/src/Grav/Framework/RequestHandler/RequestHandler.php b/system/src/Grav/Framework/RequestHandler/RequestHandler.php new file mode 100644 index 0000000..5b03805 --- /dev/null +++ b/system/src/Grav/Framework/RequestHandler/RequestHandler.php @@ -0,0 +1,70 @@ +middleware = $middleware; + $this->handler = $default; + $this->container = $container; + } + + /** + * Add callable initializing Middleware that will be executed as soon as possible. + * + * @param string $name + * @param callable $callable + * @return $this + */ + public function addCallable(string $name, callable $callable): self + { + $this->container[$name] = $callable; + array_unshift($this->middleware, $name); + + return $this; + } + + /** + * Add Middleware that will be executed as soon as possible. + * + * @param string $name + * @param MiddlewareInterface $middleware + * @return $this + */ + public function addMiddleware(string $name, MiddlewareInterface $middleware): self + { + $this->container[$name] = $middleware; + array_unshift($this->middleware, $name); + + return $this; + } +} diff --git a/system/src/Grav/Framework/RequestHandler/Traits/RequestHandlerTrait.php b/system/src/Grav/Framework/RequestHandler/Traits/RequestHandlerTrait.php new file mode 100644 index 0000000..1b5a79e --- /dev/null +++ b/system/src/Grav/Framework/RequestHandler/Traits/RequestHandlerTrait.php @@ -0,0 +1,64 @@ + */ + protected $middleware; + + /** @var callable */ + private $handler; + + /** @var ContainerInterface|null */ + private $container; + + /** + * {@inheritdoc} + * @throws InvalidArgumentException + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $middleware = array_shift($this->middleware); + + // Use default callable if there is no middleware. + if ($middleware === null) { + return call_user_func($this->handler, $request); + } + + if ($middleware instanceof MiddlewareInterface) { + return $middleware->process($request, clone $this); + } + + if (null === $this->container || !$this->container->has($middleware)) { + throw new InvalidArgumentException( + sprintf('The middleware is not a valid %s and is not passed in the Container', MiddlewareInterface::class), + $middleware + ); + } + + array_unshift($this->middleware, $this->container->get($middleware)); + + return $this->handle($request); + } +} diff --git a/system/src/Grav/Framework/Route/Route.php b/system/src/Grav/Framework/Route/Route.php new file mode 100644 index 0000000..f9a7fa7 --- /dev/null +++ b/system/src/Grav/Framework/Route/Route.php @@ -0,0 +1,450 @@ +initParts($parts); + } + + /** + * @return array + */ + public function getParts() + { + return [ + 'path' => $this->getUriPath(true), + 'query' => $this->getUriQuery(), + 'grav' => [ + 'root' => $this->root, + 'language' => $this->language, + 'route' => $this->route, + 'extension' => $this->extension, + 'grav_params' => $this->gravParams, + 'query_params' => $this->queryParams, + ], + ]; + } + + /** + * @return string + */ + public function getRootPrefix() + { + return $this->root; + } + + /** + * @return string + */ + public function getLanguage() + { + return $this->language; + } + + /** + * @return string + */ + public function getLanguagePrefix() + { + return $this->language !== '' ? '/' . $this->language : ''; + } + + /** + * @param string|null $language + * @return string + */ + public function getBase(string $language = null): string + { + $parts = [$this->root]; + + if (null === $language) { + $language = $this->language; + } + + if ($language !== '') { + $parts[] = $language; + } + + return implode('/', $parts); + } + + /** + * @param int $offset + * @param int|null $length + * @return string + */ + public function getRoute($offset = 0, $length = null) + { + if ($offset !== 0 || $length !== null) { + return ($offset === 0 ? '/' : '') . implode('/', $this->getRouteParts($offset, $length)); + } + + return '/' . $this->route; + } + + /** + * @return string + */ + public function getExtension() + { + return $this->extension; + } + + /** + * @param int $offset + * @param int|null $length + * @return array + */ + public function getRouteParts($offset = 0, $length = null) + { + $parts = explode('/', $this->route); + + if ($offset !== 0 || $length !== null) { + $parts = array_slice($parts, $offset, $length); + } + + return $parts; + } + + /** + * Return array of both query and Grav parameters. + * + * If a parameter exists in both, prefer Grav parameter. + * + * @return array + */ + public function getParams() + { + return $this->gravParams + $this->queryParams; + } + + /** + * @return array + */ + public function getGravParams() + { + return $this->gravParams; + } + + /** + * @return array + */ + public function getQueryParams() + { + return $this->queryParams; + } + + /** + * Return value of the parameter, looking into both Grav parameters and query parameters. + * + * If the parameter exists in both, return Grav parameter. + * + * @param string $param + * @return string|array|null + */ + public function getParam($param) + { + return $this->getGravParam($param) ?? $this->getQueryParam($param); + } + + /** + * @param string $param + * @return string|null + */ + public function getGravParam($param) + { + return $this->gravParams[$param] ?? null; + } + + /** + * @param string $param + * @return string|array|null + */ + public function getQueryParam($param) + { + return $this->queryParams[$param] ?? null; + } + + /** + * Allow the ability to set the route to something else + * + * @param string $route + * @return Route + */ + public function withRoute($route) + { + $new = $this->copy(); + $new->route = $route; + + return $new; + } + + /** + * Allow the ability to set the root to something else + * + * @param string $root + * @return Route + */ + public function withRoot($root) + { + $new = $this->copy(); + $new->root = $root; + + return $new; + } + + /** + * @param string|null $language + * @return Route + */ + public function withLanguage($language) + { + $new = $this->copy(); + $new->language = $language ?? ''; + + return $new; + } + + /** + * @param string $path + * @return Route + */ + public function withAddedPath($path) + { + $new = $this->copy(); + $new->route .= '/' . ltrim($path, '/'); + + return $new; + } + + /** + * @param string $extension + * @return Route + */ + public function withExtension($extension) + { + $new = $this->copy(); + $new->extension = $extension; + + return $new; + } + + /** + * @param string $param + * @param mixed $value + * @return Route + */ + public function withGravParam($param, $value) + { + return $this->withParam('gravParams', $param, null !== $value ? (string)$value : null); + } + + /** + * @param string $param + * @param mixed $value + * @return Route + */ + public function withQueryParam($param, $value) + { + return $this->withParam('queryParams', $param, $value); + } + + /** + * @return Route + */ + public function withoutParams() + { + return $this->withoutGravParams()->withoutQueryParams(); + } + + /** + * @return Route + */ + public function withoutGravParams() + { + $new = $this->copy(); + $new->gravParams = []; + + return $new; + } + + /** + * @return Route + */ + public function withoutQueryParams() + { + $new = $this->copy(); + $new->queryParams = []; + + return $new; + } + + /** + * @return \Grav\Framework\Uri\Uri + */ + public function getUri() + { + return UriFactory::createFromParts($this->getParts()); + } + + /** + * @param bool $includeRoot + * @return string + */ + public function toString(bool $includeRoot = false) + { + $url = $this->getUriPath($includeRoot); + + if ($this->queryParams) { + $url .= '?' . $this->getUriQuery(); + } + + return rtrim($url,'/'); + } + + /** + * @return string + * @deprecated 1.6 Use ->toString(true) or ->getUri() instead. + */ + public function __toString() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() will change in the future to return route, not relative url: use ->toString(true) or ->getUri() instead.', E_USER_DEPRECATED); + + return $this->toString(true); + } + + /** + * @param string $type + * @param string $param + * @param mixed $value + * @return Route + */ + protected function withParam($type, $param, $value) + { + $values = $this->{$type} ?? []; + $oldValue = $values[$param] ?? null; + + if ($oldValue === $value) { + return $this; + } + + $new = $this->copy(); + if ($value === null) { + unset($values[$param]); + } else { + $values[$param] = $value; + } + + $new->{$type} = $values; + + return $new; + } + + /** + * @return Route + */ + protected function copy() + { + return clone $this; + } + + /** + * @param bool $includeRoot + * @return string + */ + protected function getUriPath($includeRoot = false) + { + $parts = $includeRoot ? [$this->root] : ['']; + + if ($this->language !== '') { + $parts[] = $this->language; + } + + $parts[] = $this->extension ? $this->route . '.' . $this->extension : $this->route; + + + if ($this->gravParams) { + $parts[] = RouteFactory::buildParams($this->gravParams); + } + + return implode('/', $parts); + } + + /** + * @return string + */ + protected function getUriQuery() + { + return UriFactory::buildQuery($this->queryParams); + } + + /** + * @param array $parts + * @return void + */ + protected function initParts(array $parts) + { + if (isset($parts['grav'])) { + $gravParts = $parts['grav']; + $this->root = $gravParts['root']; + $this->language = $gravParts['language']; + $this->route = $gravParts['route']; + $this->extension = $gravParts['extension'] ?? ''; + $this->gravParams = $gravParts['params'] ?? []; + $this->queryParams = $parts['query_params'] ?? []; + } else { + $this->root = RouteFactory::getRoot(); + $this->language = RouteFactory::getLanguage(); + + $path = $parts['path'] ?? '/'; + if (isset($parts['params'])) { + $this->route = trim(rawurldecode($path), '/'); + $this->gravParams = $parts['params']; + } else { + $this->route = trim(RouteFactory::stripParams($path, true), '/'); + $this->gravParams = RouteFactory::getParams($path); + } + if (isset($parts['query'])) { + $this->queryParams = UriFactory::parseQuery($parts['query']); + } + } + } +} diff --git a/system/src/Grav/Framework/Route/RouteFactory.php b/system/src/Grav/Framework/Route/RouteFactory.php new file mode 100644 index 0000000..68baf55 --- /dev/null +++ b/system/src/Grav/Framework/Route/RouteFactory.php @@ -0,0 +1,236 @@ +toArray(); + $parts += [ + 'grav' => [] + ]; + $path = $parts['path'] ?? ''; + $parts['grav'] += [ + 'root' => self::$root, + 'language' => self::$language, + 'route' => trim($path, '/'), + 'params' => $parts['params'] ?? [], + ]; + + return static::createFromParts($parts); + } + + /** + * @param string $path + * @return Route + */ + public static function createFromString(string $path): Route + { + $path = ltrim($path, '/'); + if (self::$language && mb_strpos($path, self::$language) === 0) { + $path = ltrim(mb_substr($path, mb_strlen(self::$language)), '/'); + } + + $parts = [ + 'path' => $path, + 'query' => '', + 'query_params' => [], + 'grav' => [ + 'root' => self::$root, + 'language' => self::$language, + 'route' => static::trimParams($path), + 'params' => static::getParams($path) + ], + ]; + + return new Route($parts); + } + + /** + * @return string + */ + public static function getRoot(): string + { + return self::$root; + } + + /** + * @param string $root + */ + public static function setRoot($root): void + { + self::$root = rtrim($root, '/'); + } + + /** + * @return string + */ + public static function getLanguage(): string + { + return self::$language; + } + + /** + * @param string $language + */ + public static function setLanguage(string $language): void + { + self::$language = trim($language, '/'); + } + + /** + * @return string + */ + public static function getParamValueDelimiter(): string + { + return self::$delimiter; + } + + /** + * @param string $delimiter + */ + public static function setParamValueDelimiter(string $delimiter): void + { + self::$delimiter = $delimiter ?: ':'; + } + + /** + * @param array $params + * @return string + */ + public static function buildParams(array $params): string + { + if (!$params) { + return ''; + } + + $delimiter = self::$delimiter; + + $output = []; + foreach ($params as $key => $value) { + $output[] = "{$key}{$delimiter}{$value}"; + } + + return implode('/', $output); + } + + /** + * @param string $path + * @param bool $decode + * @return string + */ + public static function stripParams(string $path, bool $decode = false): string + { + $pos = strpos($path, self::$delimiter); + + if ($pos === false) { + return $path; + } + + $path = dirname(substr($path, 0, $pos)); + if ($path === '.') { + return ''; + } + + return $decode ? rawurldecode($path) : $path; + } + + /** + * @param string $path + * @return array + */ + public static function getParams(string $path): array + { + $params = ltrim(substr($path, strlen(static::stripParams($path))), '/'); + + return $params !== '' ? static::parseParams($params) : []; + } + + /** + * @param string $str + * @return string + */ + public static function trimParams(string $str): string + { + if ($str === '') { + return $str; + } + + $delimiter = self::$delimiter; + + /** @var array $params */ + $params = explode('/', $str); + $list = []; + foreach ($params as $param) { + if (mb_strpos($param, $delimiter) === false) { + $list[] = $param; + } + } + + return implode('/', $list); + } + + /** + * @param string $str + * @return array + */ + public static function parseParams(string $str): array + { + if ($str === '') { + return []; + } + + $delimiter = self::$delimiter; + + /** @var array $params */ + $params = explode('/', $str); + $list = []; + foreach ($params as &$param) { + /** @var array $parts */ + $parts = explode($delimiter, $param, 2); + if (isset($parts[1])) { + $var = rawurldecode($parts[0]); + $val = rawurldecode($parts[1]); + $list[$var] = $val; + } + } + + return $list; + } +} diff --git a/system/src/Grav/Framework/Session/Exceptions/SessionException.php b/system/src/Grav/Framework/Session/Exceptions/SessionException.php new file mode 100644 index 0000000..5ad948b --- /dev/null +++ b/system/src/Grav/Framework/Session/Exceptions/SessionException.php @@ -0,0 +1,20 @@ + $message, 'scope' => $scope]; + + // don't add duplicates + if (!array_key_exists($key, $this->messages)) { + $this->messages[$key] = $item; + } + + return $this; + } + + /** + * Clear message queue. + * + * @param string|null $scope + * @return $this + */ + public function clear(string $scope = null): Messages + { + if ($scope === null) { + if ($this->messages !== []) { + $this->isCleared = true; + $this->messages = []; + } + } else { + foreach ($this->messages as $key => $message) { + if ($message['scope'] === $scope) { + $this->isCleared = true; + unset($this->messages[$key]); + } + } + } + + return $this; + } + + /** + * @return bool + */ + public function isCleared(): bool + { + return $this->isCleared; + } + + /** + * Fetch all messages. + * + * @param string|null $scope + * @return array + */ + public function all(string $scope = null): array + { + if ($scope === null) { + return array_values($this->messages); + } + + $messages = []; + foreach ($this->messages as $message) { + if ($message['scope'] === $scope) { + $messages[] = $message; + } + } + + return $messages; + } + + /** + * Fetch and clear message queue. + * + * @param string|null $scope + * @return array + */ + public function fetch(string $scope = null): array + { + $messages = $this->all($scope); + $this->clear($scope); + + return $messages; + } + + /** + * @return array + */ + public function __serialize(): array + { + return [ + 'messages' => $this->messages + ]; + } + + /** + * @param array $data + * @return void + */ + public function __unserialize(array $data): void + { + $this->messages = $data['messages']; + } +} diff --git a/system/src/Grav/Framework/Session/Session.php b/system/src/Grav/Framework/Session/Session.php new file mode 100644 index 0000000..3feae5a --- /dev/null +++ b/system/src/Grav/Framework/Session/Session.php @@ -0,0 +1,536 @@ +isSessionStarted()) { + session_unset(); + session_destroy(); + } + + // Set default options. + $options += [ + 'cache_limiter' => 'nocache', + 'use_trans_sid' => 0, + 'use_cookies' => 1, + 'lazy_write' => 1, + 'use_strict_mode' => 1 + ]; + + $this->setOptions($options); + + session_register_shutdown(); + + self::$instance = $this; + } + + /** + * @inheritdoc + */ + public function getId() + { + return session_id() ?: null; + } + + /** + * @inheritdoc + */ + public function setId($id) + { + session_id($id); + + return $this; + } + + /** + * @inheritdoc + */ + public function getName() + { + return session_name() ?: null; + } + + /** + * @inheritdoc + */ + public function setName($name) + { + session_name($name); + + return $this; + } + + /** + * @inheritdoc + */ + public function setOptions(array $options) + { + if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { + return; + } + + $allowedOptions = [ + 'save_path' => true, + 'name' => true, + 'save_handler' => true, + 'gc_probability' => true, + 'gc_divisor' => true, + 'gc_maxlifetime' => true, + 'serialize_handler' => true, + 'cookie_lifetime' => true, + 'cookie_path' => true, + 'cookie_domain' => true, + 'cookie_secure' => true, + 'cookie_httponly' => true, + 'use_strict_mode' => true, + 'use_cookies' => true, + 'use_only_cookies' => true, + 'cookie_samesite' => true, + 'referer_check' => true, + 'cache_limiter' => true, + 'cache_expire' => true, + 'use_trans_sid' => true, + 'trans_sid_tags' => true, + 'trans_sid_hosts' => true, + 'sid_length' => true, + 'sid_bits_per_character' => true, + 'upload_progress.enabled' => true, + 'upload_progress.cleanup' => true, + 'upload_progress.prefix' => true, + 'upload_progress.name' => true, + 'upload_progress.freq' => true, + 'upload_progress.min-freq' => true, + 'lazy_write' => true + ]; + + foreach ($options as $key => $value) { + if (is_array($value)) { + // Allow nested options. + foreach ($value as $key2 => $value2) { + $ckey = "{$key}.{$key2}"; + if (isset($value2, $allowedOptions[$ckey])) { + $this->setOption($ckey, $value2); + } + } + } elseif (isset($value, $allowedOptions[$key])) { + $this->setOption($key, $value); + } + } + } + + /** + * @inheritdoc + */ + public function start($readonly = false) + { + if (\PHP_SAPI === 'cli') { + return $this; + } + + $sessionName = session_name(); + $sessionExists = isset($_COOKIE[$sessionName]); + + // Protection against invalid session cookie names throwing exception: http://php.net/manual/en/function.session-id.php#116836 + if ($sessionExists && !preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $_COOKIE[$sessionName])) { + unset($_COOKIE[$sessionName]); + $sessionExists = false; + } + + $options = $this->options; + if ($readonly) { + $options['read_and_close'] = '1'; + } + + try { + $success = @session_start($options); + if (!$success) { + $last = error_get_last(); + $error = $last ? $last['message'] : 'Unknown error'; + + throw new RuntimeException($error); + } + + // Handle changing session id. + if ($this->__isset('session_destroyed')) { + $newId = $this->__get('session_new_id'); + if (!$newId || $this->__get('session_destroyed') < time() - 300) { + // Should not happen usually. This could be attack or due to unstable network. Destroy this session. + $this->invalidate(); + + throw new RuntimeException('Obsolete session access.', 500); + } + + // Not fully expired yet. Could be lost cookie by unstable network. Start session with new session id. + session_write_close(); + + // Start session with new session id. + $useStrictMode = $options['use_strict_mode'] ?? 0; + if ($useStrictMode) { + ini_set('session.use_strict_mode', '0'); + } + session_id($newId); + if ($useStrictMode) { + ini_set('session.use_strict_mode', '1'); + } + + $success = @session_start($options); + if (!$success) { + $last = error_get_last(); + $error = $last ? $last['message'] : 'Unknown error'; + + throw new RuntimeException($error); + } + } + } catch (Exception $e) { + throw new SessionException('Failed to start session: ' . $e->getMessage(), 500); + } + + $this->started = true; + $this->onSessionStart(); + + $user = $this->__get('user'); + if ($user && (!$user instanceof UserInterface || (method_exists($user, 'isValid') && !$user->isValid()))) { + $this->invalidate(); + + throw new SessionException('Invalid User object, session destroyed.', 500); + } + + // Extend the lifetime of the session. + if ($sessionExists) { + $this->setCookie(); + } + + return $this; + } + + /** + * Regenerate session id but keep the current session information. + * + * Session id must be regenerated on login, logout or after long time has been passed. + * + * @return $this + * @since 1.7 + */ + public function regenerateId() + { + if (!$this->isSessionStarted()) { + return $this; + } + + // TODO: session_create_id() segfaults in PHP 7.3 (PHP bug #73461), remove phpstan rule when removing this one. + if (PHP_VERSION_ID < 70400) { + $newId = 0; + } else { + // Session id creation may fail with some session storages. + $newId = @session_create_id() ?: 0; + } + + // Set destroyed timestamp for the old session as well as pointer to the new id. + $this->__set('session_destroyed', time()); + $this->__set('session_new_id', $newId); + + // Keep the old session alive to avoid lost sessions by unstable network. + if (!$newId) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addMessage('Session fixation lost session detection is turned of due to server limitations.', 'warning'); + + session_regenerate_id(false); + } else { + session_write_close(); + + // Start session with new session id. + $useStrictMode = $this->options['use_strict_mode'] ?? 0; + if ($useStrictMode) { + ini_set('session.use_strict_mode', '0'); + } + session_id($newId); + if ($useStrictMode) { + ini_set('session.use_strict_mode', '1'); + } + + $this->removeCookie(); + + $success = @session_start($this->options); + if (!$success) { + $last = error_get_last(); + $error = $last ? $last['message'] : 'Unknown error'; + + throw new RuntimeException($error); + } + + $this->onSessionStart(); + } + + // New session does not have these. + $this->__unset('session_destroyed'); + $this->__unset('session_new_id'); + + return $this; + } + + /** + * @inheritdoc + */ + public function invalidate() + { + $name = $this->getName(); + if (null !== $name) { + $this->removeCookie(); + + setcookie( + session_name(), + '', + $this->getCookieOptions(-42000) + ); + } + + if ($this->isSessionStarted()) { + session_unset(); + session_destroy(); + } + + $this->started = false; + + return $this; + } + + /** + * @inheritdoc + */ + public function close() + { + if ($this->started) { + session_write_close(); + } + + $this->started = false; + + return $this; + } + + /** + * @inheritdoc + */ + public function clear() + { + session_unset(); + + return $this; + } + + /** + * @inheritdoc + */ + public function getAll() + { + return $_SESSION; + } + + /** + * @inheritdoc + */ + public function getIterator() + { + return new ArrayIterator($_SESSION); + } + + /** + * @inheritdoc + */ + public function isStarted() + { + return $this->started; + } + + /** + * @inheritdoc + */ + public function __isset($name) + { + return isset($_SESSION[$name]); + } + + /** + * @inheritdoc + */ + public function __get($name) + { + return $_SESSION[$name] ?? null; + } + + /** + * @inheritdoc + */ + public function __set($name, $value) + { + $_SESSION[$name] = $value; + } + + /** + * @inheritdoc + */ + public function __unset($name) + { + unset($_SESSION[$name]); + } + + /** + * http://php.net/manual/en/function.session-status.php#113468 + * Check if session is started nicely. + * @return bool + */ + protected function isSessionStarted() + { + return \PHP_SAPI !== 'cli' ? \PHP_SESSION_ACTIVE === session_status() : false; + } + + protected function onSessionStart(): void + { + } + + /** + * Store something in cookie temporarily. + * + * @param int|null $lifetime + * @return array + */ + public function getCookieOptions(int $lifetime = null): array + { + $params = session_get_cookie_params(); + + return [ + 'expires' => time() + ($lifetime ?? $params['lifetime']), + 'path' => $params['path'], + 'domain' => $params['domain'], + 'secure' => $params['secure'], + 'httponly' => $params['httponly'], + 'samesite' => $params['samesite'] + ]; + } + + /** + * @return void + */ + protected function setCookie(): void + { + $this->removeCookie(); + + setcookie( + session_name(), + session_id(), + $this->getCookieOptions() + ); + } + + protected function removeCookie(): void + { + $search = " {$this->getName()}="; + $cookies = []; + $found = false; + + foreach (headers_list() as $header) { + // Identify cookie headers + if (strpos($header, 'Set-Cookie:') === 0) { + // Add all but session cookie(s). + if (!str_contains($header, $search)) { + $cookies[] = $header; + } else { + $found = true; + } + } + } + + // Nothing to do. + if (false === $found) { + return; + } + + // Remove all cookies and put back all but session cookie. + header_remove('Set-Cookie'); + foreach($cookies as $cookie) { + header($cookie, false); + } + } + + /** + * @param string $key + * @param mixed $value + * @return void + */ + protected function setOption($key, $value) + { + if (!is_string($value)) { + if (is_bool($value)) { + $value = $value ? '1' : '0'; + } else { + $value = (string)$value; + } + } + + $this->options[$key] = $value; + ini_set("session.{$key}", $value); + } +} diff --git a/system/src/Grav/Framework/Session/SessionInterface.php b/system/src/Grav/Framework/Session/SessionInterface.php new file mode 100644 index 0000000..43ca40f --- /dev/null +++ b/system/src/Grav/Framework/Session/SessionInterface.php @@ -0,0 +1,152 @@ +initParts($parts); + } + + /** + * @return string + */ + public function getUser() + { + return parent::getUser(); + } + + /** + * @return string + */ + public function getPassword() + { + return parent::getPassword(); + } + + /** + * @return array + */ + public function getParts() + { + return parent::getParts(); + } + + /** + * @return string + */ + public function getUrl() + { + return parent::getUrl(); + } + + /** + * @return string + */ + public function getBaseUrl() + { + return parent::getBaseUrl(); + } + + /** + * @param string $key + * @return string|null + */ + public function getQueryParam($key) + { + $queryParams = $this->getQueryParams(); + + return $queryParams[$key] ?? null; + } + + /** + * @param string $key + * @return UriInterface + */ + public function withoutQueryParam($key) + { + return GuzzleUri::withoutQueryValue($this, $key); + } + + /** + * @param string $key + * @param string|null $value + * @return UriInterface + */ + public function withQueryParam($key, $value) + { + return GuzzleUri::withQueryValue($this, $key, $value); + } + + /** + * @return array + */ + public function getQueryParams() + { + if ($this->queryParams === null) { + $this->queryParams = UriFactory::parseQuery($this->getQuery()); + } + + return $this->queryParams; + } + + /** + * @param array $params + * @return UriInterface + */ + public function withQueryParams(array $params) + { + $query = UriFactory::buildQuery($params); + + return $this->withQuery($query); + } + + /** + * Whether the URI has the default port of the current scheme. + * + * `$uri->getPort()` may return the standard port. This method can be used for some non-http/https Uri. + * + * @return bool + */ + public function isDefaultPort() + { + return $this->getPort() === null || GuzzleUri::isDefaultPort($this); + } + + /** + * Whether the URI is absolute, i.e. it has a scheme. + * + * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true + * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative + * to another URI, the base URI. Relative references can be divided into several forms: + * - network-path references, e.g. '//example.com/path' + * - absolute-path references, e.g. '/path' + * - relative-path references, e.g. 'subpath' + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4 + */ + public function isAbsolute() + { + return GuzzleUri::isAbsolute($this); + } + + /** + * Whether the URI is a network-path reference. + * + * A relative reference that begins with two slash characters is termed an network-path reference. + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public function isNetworkPathReference() + { + return GuzzleUri::isNetworkPathReference($this); + } + + /** + * Whether the URI is a absolute-path reference. + * + * A relative reference that begins with a single slash character is termed an absolute-path reference. + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public function isAbsolutePathReference() + { + return GuzzleUri::isAbsolutePathReference($this); + } + + /** + * Whether the URI is a relative-path reference. + * + * A relative reference that does not begin with a slash character is termed a relative-path reference. + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public function isRelativePathReference() + { + return GuzzleUri::isRelativePathReference($this); + } + + /** + * Whether the URI is a same-document reference. + * + * A same-document reference refers to a URI that is, aside from its fragment + * component, identical to the base URI. When no base URI is given, only an empty + * URI reference (apart from its fragment) is considered a same-document reference. + * + * @param UriInterface|null $base An optional base URI to compare against + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.4 + */ + public function isSameDocumentReference(UriInterface $base = null) + { + return GuzzleUri::isSameDocumentReference($this, $base); + } +} diff --git a/system/src/Grav/Framework/Uri/UriFactory.php b/system/src/Grav/Framework/Uri/UriFactory.php new file mode 100644 index 0000000..f7724b7 --- /dev/null +++ b/system/src/Grav/Framework/Uri/UriFactory.php @@ -0,0 +1,171 @@ + $scheme, + 'user' => $user, + 'pass' => $pass, + 'host' => $host, + 'port' => $port, + 'path' => $path, + 'query' => $query + ]; + } + + /** + * UTF-8 aware parse_url() implementation. + * + * @param string $url + * @return array + * @throws InvalidArgumentException + */ + public static function parseUrl($url) + { + if (!is_string($url)) { + throw new InvalidArgumentException('URL must be a string'); + } + + $encodedUrl = preg_replace_callback( + '%[^:/@?&=#]+%u', + function ($matches) { + return rawurlencode($matches[0]); + }, + $url + ); + + $parts = is_string($encodedUrl) ? parse_url($encodedUrl) : false; + if ($parts === false) { + throw new InvalidArgumentException("Malformed URL: {$url}"); + } + + return $parts; + } + + /** + * Parse query string and return it as an array. + * + * @param string $query + * @return mixed + */ + public static function parseQuery($query) + { + parse_str($query, $params); + + return $params; + } + + /** + * Build query string from variables. + * + * @param array $params + * @return string + */ + public static function buildQuery(array $params) + { + if (!$params) { + return ''; + } + + $separator = ini_get('arg_separator.output') ?: '&'; + + return http_build_query($params, '', $separator, PHP_QUERY_RFC3986); + } +} diff --git a/system/src/Grav/Framework/Uri/UriPartsFilter.php b/system/src/Grav/Framework/Uri/UriPartsFilter.php new file mode 100644 index 0000000..71a5135 --- /dev/null +++ b/system/src/Grav/Framework/Uri/UriPartsFilter.php @@ -0,0 +1,145 @@ += 0 && $port <= 65535))) { + return $port; + } + + throw new InvalidArgumentException('Uri port must be null or an integer between 0 and 65535'); + } + + /** + * Filter Uri path. + * + * This method percent-encodes all reserved characters in the provided path string. This method + * will NOT double-encode characters that are already percent-encoded. + * + * @param string $path The raw uri path. + * @return string The RFC 3986 percent-encoded uri path. + * @throws InvalidArgumentException If the path is invalid. + * @link http://www.faqs.org/rfcs/rfc3986.html + */ + public static function filterPath($path) + { + if (!is_string($path)) { + throw new InvalidArgumentException('Uri path must be a string'); + } + + return preg_replace_callback( + '/(?:[^a-zA-Z0-9_\-\.~:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/u', + function ($match) { + return rawurlencode($match[0]); + }, + $path + ) ?? ''; + } + + /** + * Filters the query string or fragment of a URI. + * + * @param string $query The raw uri query string. + * @return string The percent-encoded query string. + * @throws InvalidArgumentException If the query is invalid. + */ + public static function filterQueryOrFragment($query) + { + if (!is_string($query)) { + throw new InvalidArgumentException('Uri query string and fragment must be a string'); + } + + return preg_replace_callback( + '/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/u', + function ($match) { + return rawurlencode($match[0]); + }, + $query + ) ?? ''; + } +} diff --git a/system/src/Grav/Installer/Install.php b/system/src/Grav/Installer/Install.php new file mode 100644 index 0000000..4df9dcd --- /dev/null +++ b/system/src/Grav/Installer/Install.php @@ -0,0 +1,392 @@ + [ + 'name' => 'PHP', + 'versions' => [ + '7.4' => '7.4.0', + '7.3' => '7.3.6', + '' => '7.4.12' + ] + ], + 'grav' => [ + 'name' => 'Grav', + 'versions' => [ + '1.6' => '1.6.0', + '' => '1.6.28' + ] + ], + 'plugins' => [ + 'admin' => [ + 'name' => 'Admin', + 'optional' => true, + 'versions' => [ + '1.9' => '1.9.0', + '' => '1.9.13' + ] + ], + 'email' => [ + 'name' => 'Email', + 'optional' => true, + 'versions' => [ + '3.0' => '3.0.0', + '' => '3.0.10' + ] + ], + 'form' => [ + 'name' => 'Form', + 'optional' => true, + 'versions' => [ + '4.1' => '4.1.0', + '4.0' => '4.0.0', + '3.0' => '3.0.0', + '' => '4.1.2' + ] + ], + 'login' => [ + 'name' => 'Login', + 'optional' => true, + 'versions' => [ + '3.3' => '3.3.0', + '3.0' => '3.0.0', + '' => '3.3.6' + ] + ], + ] + ]; + + /** @var array */ + public $ignores = [ + 'backup', + 'cache', + 'images', + 'logs', + 'tmp', + 'user', + '.htaccess', + 'robots.txt' + ]; + + /** @var array */ + private $classMap = [ + InstallException::class => __DIR__ . '/InstallException.php', + Versions::class => __DIR__ . '/Versions.php', + VersionUpdate::class => __DIR__ . '/VersionUpdate.php', + VersionUpdater::class => __DIR__ . '/VersionUpdater.php', + YamlUpdater::class => __DIR__ . '/YamlUpdater.php', + ]; + + /** @var string|null */ + private $zip; + + /** @var string|null */ + private $location; + + /** @var VersionUpdater */ + private $updater; + + /** @var static */ + private static $instance; + + /** + * @return static + */ + public static function instance() + { + if (null === self::$instance) { + self::$instance = new static(); + } + + return self::$instance; + } + + private function __construct() + { + } + + /** + * @param string|null $zip + * @return $this + */ + public function setZip(?string $zip) + { + $this->zip = $zip; + + return $this; + } + + /** + * @param string|null $zip + * @return void + */ + public function __invoke(?string $zip) + { + $this->zip = $zip; + + $failedRequirements = $this->checkRequirements(); + if ($failedRequirements) { + $error = ['Following requirements have failed:']; + + foreach ($failedRequirements as $name => $req) { + $error[] = "{$req['title']} >= v{$req['minimum']} required, you have v{$req['installed']}"; + } + + $errors = implode("
    \n", $error); + if (\defined('GRAV_CLI') && GRAV_CLI) { + $errors = "\n\n" . strip_tags($errors) . "\n\n"; + $errors .= <<prepare(); + $this->install(); + $this->finalize(); + } + + /** + * NOTE: This method can only be called after $grav['plugins']->init(). + * + * @return array List of failed requirements. If the list is empty, installation can go on. + */ + public function checkRequirements(): array + { + $results = []; + + $this->checkVersion($results, 'php', 'php', $this->requires['php'], PHP_VERSION); + $this->checkVersion($results, 'grav', 'grav', $this->requires['grav'], GRAV_VERSION); + $this->checkPlugins($results, $this->requires['plugins']); + + return $results; + } + + /** + * @return void + * @throws RuntimeException + */ + public function prepare(): void + { + // Locate the new Grav update and the target site from the filesystem. + $location = realpath(__DIR__); + $target = realpath(GRAV_ROOT . '/index.php'); + + if (!$location) { + throw new RuntimeException('Internal Error', 500); + } + + if ($target && dirname($location, 4) === dirname($target)) { + // We cannot copy files into themselves, abort! + throw new RuntimeException('Grav has already been installed here!', 400); + } + + // Load the installer classes. + foreach ($this->classMap as $class_name => $path) { + // Make sure that none of the Grav\Installer classes have been loaded, otherwise installation may fail! + if (class_exists($class_name, false)) { + throw new RuntimeException(sprintf('Cannot update Grav, class %s has already been loaded!', $class_name), 500); + } + + require $path; + } + + $this->legacySupport(); + + $this->location = dirname($location, 4); + + $versions = Versions::instance(USER_DIR . 'config/versions.yaml'); + $this->updater = new VersionUpdater('core/grav', __DIR__ . '/updates', $this->getVersion(), $versions); + + $this->updater->preflight(); + } + + /** + * @return void + * @throws RuntimeException + */ + public function install(): void + { + if (!$this->location) { + throw new RuntimeException('Oops, installer was run without prepare()!', 500); + } + + try { + // Update user/config/version.yaml before copying the files to avoid frontend from setting the version schema. + $this->updater->install(); + + Installer::install( + $this->zip ?? '', + GRAV_ROOT, + ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true, 'ignores' => $this->ignores], + $this->location, + !($this->zip && is_file($this->zip)) + ); + } catch (Exception $e) { + Installer::setError($e->getMessage()); + } + + $errorCode = Installer::lastErrorCode(); + + $success = !(is_string($errorCode) || ($errorCode & (Installer::ZIP_OPEN_ERROR | Installer::ZIP_EXTRACT_ERROR))); + + if (!$success) { + throw new RuntimeException(Installer::lastErrorMsg()); + } + } + + /** + * @return void + * @throws RuntimeException + */ + public function finalize(): void + { + // Finalize can be run without installing Grav first. + if (!$this->updater) { + $versions = Versions::instance(USER_DIR . 'config/versions.yaml'); + $this->updater = new VersionUpdater('core/grav', __DIR__ . '/updates', GRAV_VERSION, $versions); + $this->updater->install(); + } + + $this->updater->postflight(); + + Cache::clearCache('all'); + + clearstatcache(); + if (function_exists('opcache_reset')) { + @opcache_reset(); + } + } + + /** + * @param array $results + * @param string $type + * @param string $name + * @param array $check + * @param string|null $version + * @return void + */ + protected function checkVersion(array &$results, $type, $name, array $check, $version): void + { + if (null === $version && !empty($check['optional'])) { + return; + } + + $major = $minor = 0; + $versions = $check['versions'] ?? []; + foreach ($versions as $major => $minor) { + if (!$major || version_compare($version ?? '0', $major, '<')) { + continue; + } + + if (version_compare($version ?? '0', $minor, '>=')) { + return; + } + + break; + } + + if (!$major) { + $minor = reset($versions); + } + + $recommended = end($versions); + + if (version_compare($recommended, $minor, '<=')) { + $recommended = null; + } + + $results[$name] = [ + 'type' => $type, + 'name' => $name, + 'title' => $check['name'] ?? $name, + 'installed' => $version, + 'minimum' => $minor, + 'recommended' => $recommended + ]; + } + + /** + * @param array $results + * @param array $plugins + * @return void + */ + protected function checkPlugins(array &$results, array $plugins): void + { + if (!class_exists('Plugins')) { + return; + } + + foreach ($plugins as $name => $check) { + $plugin = Plugins::get($name); + if (!$plugin) { + $this->checkVersion($results, 'plugin', $name, $check, null); + continue; + } + + $blueprint = $plugin->blueprints(); + $version = (string)$blueprint->get('version'); + $check['name'] = ($blueprint->get('name') ?? $check['name'] ?? $name) . ' Plugin'; + $this->checkVersion($results, 'plugin', $name, $check, $version); + } + } + + /** + * @return string + */ + protected function getVersion(): string + { + $definesFile = "{$this->location}/system/defines.php"; + $content = file_get_contents($definesFile); + if (false === $content) { + return ''; + } + + preg_match("/define\('GRAV_VERSION', '([^']+)'\);/mu", $content, $matches); + + return $matches[1] ?? ''; + } + + protected function legacySupport(): void + { + // Support install for Grav 1.6.0 - 1.6.20 by loading the original class from the older version of Grav. + class_exists(\Grav\Console\Cli\CacheCommand::class, true); + } +} diff --git a/system/src/Grav/Installer/InstallException.php b/system/src/Grav/Installer/InstallException.php new file mode 100644 index 0000000..145b7c9 --- /dev/null +++ b/system/src/Grav/Installer/InstallException.php @@ -0,0 +1,29 @@ +getCode(), $previous); + } +} diff --git a/system/src/Grav/Installer/VersionUpdate.php b/system/src/Grav/Installer/VersionUpdate.php new file mode 100644 index 0000000..faaf2a2 --- /dev/null +++ b/system/src/Grav/Installer/VersionUpdate.php @@ -0,0 +1,82 @@ +revision = $name; + [$this->version, $this->date, $this->patch] = explode('_', $name); + $this->updater = $updater; + $this->methods = require $file; + } + + public function getRevision(): string + { + return $this->revision; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getDate(): string + { + return $this->date; + } + + public function getPatch(): string + { + return $this->date; + } + + public function getUpdater(): VersionUpdater + { + return $this->updater; + } + + /** + * Run right before installation. + */ + public function preflight(VersionUpdater $updater): void + { + $method = $this->methods['preflight'] ?? null; + if ($method instanceof Closure) { + $method->call($this); + } + } + + /** + * Runs right after installation. + */ + public function postflight(VersionUpdater $updater): void + { + $method = $this->methods['postflight'] ?? null; + if ($method instanceof Closure) { + $method->call($this); + } + } +} diff --git a/system/src/Grav/Installer/VersionUpdater.php b/system/src/Grav/Installer/VersionUpdater.php new file mode 100644 index 0000000..75a3b04 --- /dev/null +++ b/system/src/Grav/Installer/VersionUpdater.php @@ -0,0 +1,133 @@ +name = $name; + $this->path = $path; + $this->version = $version; + $this->versions = $versions; + + $this->loadUpdates(); + } + + /** + * Pre-installation method. + */ + public function preflight(): void + { + foreach ($this->updates as $revision => $update) { + $update->preflight($this); + } + } + + /** + * Install method. + */ + public function install(): void + { + $versions = $this->getVersions(); + $versions->updateVersion($this->name, $this->version); + $versions->save(); + } + + /** + * Post-installation method. + */ + public function postflight(): void + { + $versions = $this->getVersions(); + + foreach ($this->updates as $revision => $update) { + $update->postflight($this); + + $versions->setSchema($this->name, $revision); + $versions->save(); + } + } + + /** + * @return Versions + */ + public function getVersions(): Versions + { + return $this->versions; + } + + /** + * @param string|null $name + * @return string|null + */ + public function getExtensionVersion(string $name = null): ?string + { + return $this->versions->getVersion($name ?? $this->name); + } + + /** + * @param string|null $name + * @return string|null + */ + public function getExtensionSchema(string $name = null): ?string + { + return $this->versions->getSchema($name ?? $this->name); + } + + /** + * @param string|null $name + * @return array + */ + public function getExtensionHistory(string $name = null): array + { + return $this->versions->getHistory($name ?? $this->name); + } + + protected function loadUpdates(): void + { + $this->updates = []; + + $schema = $this->getExtensionSchema(); + $iterator = new DirectoryIterator($this->path); + foreach ($iterator as $item) { + if (!$item->isFile() || $item->getExtension() !== 'php') { + continue; + } + + $revision = $item->getBasename('.php'); + if (!$schema || version_compare($revision, $schema, '>')) { + $realPath = $item->getRealPath(); + if ($realPath) { + $this->updates[$revision] = new VersionUpdate($realPath, $this); + } + } + } + + uksort($this->updates, 'version_compare'); + } +} diff --git a/system/src/Grav/Installer/Versions.php b/system/src/Grav/Installer/Versions.php new file mode 100644 index 0000000..03f3b0b --- /dev/null +++ b/system/src/Grav/Installer/Versions.php @@ -0,0 +1,329 @@ +updated) { + return false; + } + + file_put_contents($this->filename, Yaml::dump($this->items, 4, 2)); + + $this->updated = false; + + return true; + } + + /** + * @return array + */ + public function getAll(): array + { + return $this->items; + } + + /** + * @return array|null + */ + public function getGrav(): ?array + { + return $this->get('core/grav'); + } + + /** + * @return array + */ + public function getPlugins(): array + { + return $this->get('plugins', []); + } + + /** + * @param string $name + * @return array|null + */ + public function getPlugin(string $name): ?array + { + return $this->get("plugins/{$name}"); + } + + /** + * @return array + */ + public function getThemes(): array + { + return $this->get('themes', []); + } + + /** + * @param string $name + * @return array|null + */ + public function getTheme(string $name): ?array + { + return $this->get("themes/{$name}"); + } + + /** + * @param string $extension + * @return array|null + */ + public function getExtension(string $extension): ?array + { + return $this->get($extension); + } + + /** + * @param string $extension + * @param array|null $value + */ + public function setExtension(string $extension, ?array $value): void + { + if (null !== $value) { + $this->set($extension, $value); + } else { + $this->undef($extension); + } + } + + /** + * @param string $extension + * @return string|null + */ + public function getVersion(string $extension): ?string + { + $version = $this->get("{$extension}/version", null); + + return is_string($version) ? $version : null; + } + + /** + * @param string $extension + * @param string|null $version + */ + public function setVersion(string $extension, ?string $version): void + { + $this->updateHistory($extension, $version); + } + + /** + * NOTE: Updates also history. + * + * @param string $extension + * @param string|null $version + */ + public function updateVersion(string $extension, ?string $version): void + { + $this->set("{$extension}/version", $version); + $this->updateHistory($extension, $version); + } + + /** + * @param string $extension + * @return string|null + */ + public function getSchema(string $extension): ?string + { + $version = $this->get("{$extension}/schema", null); + + return is_string($version) ? $version : null; + } + + /** + * @param string $extension + * @param string|null $schema + */ + public function setSchema(string $extension, ?string $schema): void + { + if (null !== $schema) { + $this->set("{$extension}/schema", $schema); + } else { + $this->undef("{$extension}/schema"); + } + } + + /** + * @param string $extension + * @return array + */ + public function getHistory(string $extension): array + { + $name = "{$extension}/history"; + $history = $this->get($name, []); + + // Fix for broken Grav 1.6 history + if ($extension === 'grav') { + $history = $this->fixHistory($history); + } + + return $history; + } + + /** + * @param string $extension + * @param string|null $version + */ + public function updateHistory(string $extension, ?string $version): void + { + $name = "{$extension}/history"; + $history = $this->getHistory($extension); + $history[] = ['version' => $version, 'date' => gmdate('Y-m-d H:i:s')]; + $this->set($name, $history); + } + + /** + * Clears extension history. Useful when creating skeletons. + * + * @param string $extension + */ + public function removeHistory(string $extension): void + { + $this->undef("{$extension}/history"); + } + + /** + * @param array $history + * @return array + */ + private function fixHistory(array $history): array + { + if (isset($history['version'], $history['date'])) { + $fix = [['version' => $history['version'], 'date' => $history['date']]]; + unset($history['version'], $history['date']); + $history = array_merge($fix, $history); + } + + return $history; + } + + /** + * Get value by using dot notation for nested arrays/objects. + * + * @param string $name Slash separated path to the requested value. + * @param mixed $default Default value (or null). + * @return mixed Value. + */ + private function get(string $name, $default = null) + { + $path = explode('/', $name); + $current = $this->items; + + foreach ($path as $field) { + if (is_array($current) && isset($current[$field])) { + $current = $current[$field]; + } else { + return $default; + } + } + + return $current; + } + + /** + * Set value by using dot notation for nested arrays/objects. + * + * @param string $name Slash separated path to the requested value. + * @param mixed $value New value. + */ + private function set(string $name, $value): void + { + $path = explode('/', $name); + $current = &$this->items; + + foreach ($path as $field) { + // Handle arrays and scalars. + if (!is_array($current)) { + $current = [$field => []]; + } elseif (!isset($current[$field])) { + $current[$field] = []; + } + $current = &$current[$field]; + } + + $current = $value; + $this->updated = true; + } + + /** + * Unset value by using dot notation for nested arrays/objects. + * + * @param string $name Dot separated path to the requested value. + */ + private function undef(string $name): void + { + $path = $name !== '' ? explode('/', $name) : []; + if (!$path) { + return; + } + + $var = array_pop($path); + $current = &$this->items; + + foreach ($path as $field) { + if (!is_array($current) || !isset($current[$field])) { + return; + } + $current = &$current[$field]; + } + + unset($current[$var]); + $this->updated = true; + } + + private function __construct(string $filename) + { + $this->filename = $filename; + $content = is_file($filename) ? file_get_contents($filename) : null; + if (false === $content) { + throw new \RuntimeException('Versions file cannot be read'); + } + $this->items = $content ? Yaml::parse($content) : []; + } +} diff --git a/system/src/Grav/Installer/YamlUpdater.php b/system/src/Grav/Installer/YamlUpdater.php new file mode 100644 index 0000000..9af2055 --- /dev/null +++ b/system/src/Grav/Installer/YamlUpdater.php @@ -0,0 +1,430 @@ +updated) { + return false; + } + + try { + if (!$this->isHandWritten()) { + $yaml = Yaml::dump($this->items, 5, 2); + } else { + $yaml = implode("\n", $this->lines); + + $items = Yaml::parse($yaml); + if ($items !== $this->items) { + throw new \RuntimeException('Failed saving the content'); + } + } + + file_put_contents($this->filename, $yaml); + + } catch (\Exception $e) { + throw new \RuntimeException('Failed to update ' . basename($this->filename) . ': ' . $e->getMessage()); + } + + return true; + } + + /** + * @return bool + */ + public function isHandWritten(): bool + { + return !empty($this->comments); + } + + /** + * @return array + */ + public function getComments(): array + { + $comments = []; + foreach ($this->lines as $i => $line) { + if ($this->isLineEmpty($line)) { + $comments[$i+1] = $line; + } elseif ($comment = $this->getInlineComment($line)) { + $comments[$i+1] = $comment; + } + } + + return $comments; + } + + /** + * @param string $variable + * @param mixed $value + */ + public function define(string $variable, $value): void + { + // If variable has already value, we're good. + if ($this->get($variable) !== null) { + return; + } + + // If one of the parents isn't array, we're good, too. + if (!$this->canDefine($variable)) { + return; + } + + $this->set($variable, $value); + if (!$this->isHandWritten()) { + return; + } + + $parts = explode('.', $variable); + + $lineNos = $this->findPath($this->lines, $parts); + $count = count($lineNos); + $last = array_key_last($lineNos); + + $value = explode("\n", trim(Yaml::dump([$last => $this->get(implode('.', array_keys($lineNos)))], max(0, 5-$count), 2))); + $currentLine = array_pop($lineNos) ?: 0; + $parentLine = array_pop($lineNos); + + if ($parentLine !== null) { + $c = $this->getLineIndentation($this->lines[$parentLine] ?? ''); + $n = $this->getLineIndentation($this->lines[$parentLine+1] ?? $this->lines[$parentLine] ?? ''); + $indent = $n > $c ? $n : $c + 2; + } else { + $indent = 0; + array_unshift($value, ''); + } + $spaces = str_repeat(' ', $indent); + foreach ($value as &$line) { + $line = $spaces . $line; + } + unset($line); + + array_splice($this->lines, abs($currentLine)+1, 0, $value); + } + + public function undefine(string $variable): void + { + // If variable does not have value, we're good. + if ($this->get($variable) === null) { + return; + } + + // If one of the parents isn't array, we're good, too. + if (!$this->canDefine($variable)) { + return; + } + + $this->undef($variable); + if (!$this->isHandWritten()) { + return; + } + + // TODO: support also removing property from handwritten configuration file. + } + + private function __construct(string $filename) + { + $content = is_file($filename) ? (string)file_get_contents($filename) : ''; + $content = rtrim(str_replace(["\r\n", "\r"], "\n", $content)); + + $this->filename = $filename; + $this->lines = explode("\n", $content); + $this->comments = $this->getComments(); + $this->items = $content ? Yaml::parse($content) : []; + } + + /** + * Return array of offsets for the parent nodes. Negative value means position, but not found. + * + * @param array $lines + * @param array $parts + * @return int[] + */ + private function findPath(array $lines, array $parts) + { + $test = true; + $indent = -1; + $current = array_shift($parts); + + $j = 1; + $found = []; + $space = ''; + foreach ($lines as $i => $line) { + if ($this->isLineEmpty($line)) { + if ($this->isLineComment($line) && $this->getLineIndentation($line) > $indent) { + $j = $i; + } + continue; + } + + if ($test === true) { + $test = false; + $spaces = strlen($line) - strlen(ltrim($line, ' ')); + if ($spaces <= $indent) { + $found[$current] = -$j; + + return $found; + } + + $indent = $spaces; + $space = $indent ? str_repeat(' ', $indent) : ''; + } + + + if (0 === \strncmp($line, $space, strlen($space))) { + $pattern = "/^{$space}(['\"]?){$current}\\1\:/"; + + if (preg_match($pattern, $line)) { + $found[$current] = $i; + $current = array_shift($parts); + if ($current === null) { + return $found; + } + $test = true; + } + } else { + $found[$current] = -$j; + + return $found; + } + + $j = $i; + } + + $found[$current] = -$j; + + return $found; + } + + /** + * Returns true if the current line is blank or if it is a comment line. + * + * @param string $line Contents of the line + * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise + */ + private function isLineEmpty(string $line): bool + { + return $this->isLineBlank($line) || $this->isLineComment($line); + } + + /** + * Returns true if the current line is blank. + * + * @param string $line Contents of the line + * @return bool Returns true if the current line is blank, false otherwise + */ + private function isLineBlank(string $line): bool + { + return '' === trim($line, ' '); + } + + /** + * Returns true if the current line is a comment line. + * + * @param string $line Contents of the line + * @return bool Returns true if the current line is a comment line, false otherwise + */ + private function isLineComment(string $line): bool + { + //checking explicitly the first char of the trim is faster than loops or strpos + $ltrimmedLine = ltrim($line, ' '); + + return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0]; + } + + /** + * @param string $line + * @return bool + */ + private function isInlineComment(string $line): bool + { + return $this->getInlineComment($line) !== null; + } + + /** + * @param string $line + * @return string|null + */ + private function getInlineComment(string $line): ?string + { + $pos = strpos($line, ' #'); + if (false === $pos) { + return null; + } + + $parts = explode(' #', $line); + $part = ''; + while ($part .= array_shift($parts)) { + // Remove quoted values. + $part = preg_replace('/(([\'"])[^\2]*\2)/', '', $part); + assert(null !== $part); + $part = preg_split('/[\'"]/', $part, 2); + assert(false !== $part); + if (!isset($part[1])) { + $part = $part[0]; + array_unshift($parts, str_repeat(' ', strlen($part) - strlen(trim($part, ' ')))); + break; + } + $part = $part[1]; + } + + + return implode(' #', $parts); + } + + /** + * Returns the current line indentation. + * + * @param string $line + * @return int The current line indentation + */ + private function getLineIndentation(string $line): int + { + return \strlen($line) - \strlen(ltrim($line, ' ')); + } + + /** + * Get value by using dot notation for nested arrays/objects. + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @return mixed Value. + */ + private function get(string $name, $default = null) + { + $path = explode('.', $name); + $current = $this->items; + + foreach ($path as $field) { + if (is_array($current) && isset($current[$field])) { + $current = $current[$field]; + } else { + return $default; + } + } + + return $current; + } + + /** + * Set value by using dot notation for nested arrays/objects. + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value New value. + */ + private function set(string $name, $value): void + { + $path = explode('.', $name); + $current = &$this->items; + + foreach ($path as $field) { + // Handle arrays and scalars. + if (!is_array($current)) { + $current = [$field => []]; + } elseif (!isset($current[$field])) { + $current[$field] = []; + } + $current = &$current[$field]; + } + + $current = $value; + $this->updated = true; + } + + /** + * Unset value by using dot notation for nested arrays/objects. + * + * @param string $name Dot separated path to the requested value. + */ + private function undef(string $name): void + { + $path = $name !== '' ? explode('.', $name) : []; + if (!$path) { + return; + } + + $var = array_pop($path); + $current = &$this->items; + + foreach ($path as $field) { + if (!is_array($current) || !isset($current[$field])) { + return; + } + $current = &$current[$field]; + } + + unset($current[$var]); + $this->updated = true; + } + + /** + * Get value by using dot notation for nested arrays/objects. + * + * @param string $name Dot separated path to the requested value. + * @return bool + */ + private function canDefine(string $name): bool + { + $path = explode('.', $name); + $current = $this->items; + + foreach ($path as $field) { + if (is_array($current)) { + if (!isset($current[$field])) { + return true; + } + $current = $current[$field]; + } else { + return false; + } + } + + return true; + } +} diff --git a/system/src/Grav/Installer/updates/1.7.0_2020-11-20_1.php b/system/src/Grav/Installer/updates/1.7.0_2020-11-20_1.php new file mode 100644 index 0000000..6120665 --- /dev/null +++ b/system/src/Grav/Installer/updates/1.7.0_2020-11-20_1.php @@ -0,0 +1,24 @@ + null, + 'postflight' => + function () { + /** @var VersionUpdate $this */ + try { + // Keep old defaults for backwards compatibility. + $yaml = YamlUpdater::instance(GRAV_ROOT . '/user/config/system.yaml'); + $yaml->define('twig.autoescape', false); + $yaml->define('strict_mode.yaml_compat', true); + $yaml->define('strict_mode.twig_compat', true); + $yaml->define('strict_mode.blueprint_compat', true); + $yaml->save(); + } catch (\Exception $e) { + throw new InstallException('Could not update system configuration to maintain backwards compatibility', $e); + } + } +]; diff --git a/system/src/Twig/DeferredExtension/DeferredBlockNode.php b/system/src/Twig/DeferredExtension/DeferredBlockNode.php new file mode 100644 index 0000000..6ae974f --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredBlockNode.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Compiler; +use Twig\Node\BlockNode; + +final class DeferredBlockNode extends BlockNode +{ + public function compile(Compiler $compiler) : void + { + $name = $this->getAttribute('name'); + + $compiler + ->write("public function block_$name(\$context, array \$blocks = [])\n", "{\n") + ->indent() + ->write("\$this->deferred->defer(\$this, '$name');\n") + ->outdent() + ->write("}\n\n") + ; + + $compiler + ->addDebugInfo($this) + ->write("public function block_{$name}_deferred(\$context, array \$blocks = [])\n", "{\n") + ->indent() + ->subcompile($this->getNode('body')) + ->write("\$this->deferred->resolve(\$this, \$context, \$blocks);\n") + ->outdent() + ->write("}\n\n") + ; + } +} diff --git a/system/src/Twig/DeferredExtension/DeferredExtension.php b/system/src/Twig/DeferredExtension/DeferredExtension.php new file mode 100644 index 0000000..f27c2a3 --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredExtension.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Environment; +use Twig\Extension\AbstractExtension; +use Twig\Template; + +final class DeferredExtension extends AbstractExtension +{ + private $blocks = []; + + public function getTokenParsers() : array + { + return [new DeferredTokenParser()]; + } + + public function getNodeVisitors() : array + { + if (Environment::VERSION_ID < 20000) { + // Twig 1.x support + return [new DeferredNodeVisitorCompat()]; + } + + return [new DeferredNodeVisitor()]; + } + + public function defer(Template $template, string $blockName) : void + { + $templateName = $template->getTemplateName(); + $this->blocks[$templateName][] = $blockName; + $index = \count($this->blocks[$templateName]) - 1; + + \ob_start(function (string $buffer) use ($index, $templateName) { + unset($this->blocks[$templateName][$index]); + + return $buffer; + }); + } + + public function resolve(Template $template, array $context, array $blocks) : void + { + $templateName = $template->getTemplateName(); + if (empty($this->blocks[$templateName])) { + return; + } + + while ($blockName = \array_pop($this->blocks[$templateName])) { + $buffer = \ob_get_clean(); + + $blocks[$blockName] = [$template, 'block_'.$blockName.'_deferred']; + $template->displayBlock($blockName, $context, $blocks); + + echo $buffer; + } + + if ($parent = $template->getParent($context)) { + $this->resolve($parent, $context, $blocks); + } + } +} diff --git a/system/src/Twig/DeferredExtension/DeferredExtensionNode.php b/system/src/Twig/DeferredExtension/DeferredExtensionNode.php new file mode 100644 index 0000000..1b851b4 --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredExtensionNode.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Compiler; +use Twig\Node\Node; + +final class DeferredExtensionNode extends Node +{ + public function compile(Compiler $compiler) : void + { + $compiler + ->write("\$this->deferred = \$this->env->getExtension('".DeferredExtension::class."');\n") + ; + } +} diff --git a/system/src/Twig/DeferredExtension/DeferredNode.php b/system/src/Twig/DeferredExtension/DeferredNode.php new file mode 100644 index 0000000..2ac73bd --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredNode.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Compiler; +use Twig\Node\Node; + +final class DeferredNode extends Node +{ + public function compile(Compiler $compiler) : void + { + $compiler + ->write("\$this->deferred->resolve(\$this, \$context, \$blocks);\n") + ; + } +} diff --git a/system/src/Twig/DeferredExtension/DeferredNodeVisitor.php b/system/src/Twig/DeferredExtension/DeferredNodeVisitor.php new file mode 100644 index 0000000..aef7399 --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredNodeVisitor.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Environment; +use Twig\Node\ModuleNode; +use Twig\Node\Node; +use Twig\NodeVisitor\NodeVisitorInterface; + +final class DeferredNodeVisitor implements NodeVisitorInterface +{ + private $hasDeferred = false; + + public function enterNode(Node $node, Environment $env) : Node + { + if (!$this->hasDeferred && $node instanceof DeferredBlockNode) { + $this->hasDeferred = true; + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env) : ?Node + { + if ($this->hasDeferred && $node instanceof ModuleNode) { + $node->setNode('constructor_end', new Node([new DeferredExtensionNode(), $node->getNode('constructor_end')])); + $node->setNode('display_end', new Node([new DeferredNode(), $node->getNode('display_end')])); + $this->hasDeferred = false; + } + + return $node; + } + + public function getPriority() : int + { + return 0; + } +} diff --git a/system/src/Twig/DeferredExtension/DeferredNodeVisitorCompat.php b/system/src/Twig/DeferredExtension/DeferredNodeVisitorCompat.php new file mode 100644 index 0000000..8c441bb --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredNodeVisitorCompat.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Environment; +use Twig\Node\ModuleNode; +use Twig\Node\Node; +use Twig\NodeVisitor\NodeVisitorInterface; + +final class DeferredNodeVisitorCompat implements NodeVisitorInterface +{ + private $hasDeferred = false; + + public function enterNode(\Twig_NodeInterface $node, Environment $env) : Node + { + if (!$this->hasDeferred && $node instanceof DeferredBlockNode) { + $this->hasDeferred = true; + } + + return $node; + } + + public function leaveNode(\Twig_NodeInterface $node, Environment $env) : ?Node + { + if ($this->hasDeferred && $node instanceof ModuleNode) { + $node->setNode('constructor_end', new Node([new DeferredExtensionNode(), $node->getNode('constructor_end')])); + $node->setNode('display_end', new Node([new DeferredNode(), $node->getNode('display_end')])); + $this->hasDeferred = false; + } + + return $node; + } + + public function getPriority() : int + { + return 0; + } +} diff --git a/system/src/Twig/DeferredExtension/DeferredTokenParser.php b/system/src/Twig/DeferredExtension/DeferredTokenParser.php new file mode 100644 index 0000000..1870ae0 --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredTokenParser.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Node\BlockNode; +use Twig\Node\Node; +use Twig\Parser; +use Twig\Token; +use Twig\TokenParser\AbstractTokenParser; +use Twig\TokenParser\BlockTokenParser; + +final class DeferredTokenParser extends AbstractTokenParser +{ + private $blockTokenParser; + + public function setParser(Parser $parser) : void + { + parent::setParser($parser); + + $this->blockTokenParser = new BlockTokenParser(); + $this->blockTokenParser->setParser($parser); + } + + public function parse(Token $token) : Node + { + $stream = $this->parser->getStream(); + $nameToken = $stream->next(); + $deferredToken = $stream->nextIf(Token::NAME_TYPE, 'deferred'); + $stream->injectTokens([$nameToken]); + + $node = $this->blockTokenParser->parse($token); + + if ($deferredToken) { + $this->replaceBlockNode($nameToken->getValue()); + } + + return $node; + } + + public function getTag() : string + { + return 'block'; + } + + private function replaceBlockNode(string $name) : void + { + $block = $this->parser->getBlock($name)->getNode('0'); + $this->parser->setBlock($name, $this->createDeferredBlockNode($block)); + } + + private function createDeferredBlockNode(BlockNode $block) : DeferredBlockNode + { + $name = $block->getAttribute('name'); + $deferredBlock = new DeferredBlockNode($name, new Node([]), $block->getTemplateLine()); + + foreach ($block as $nodeName => $node) { + $deferredBlock->setNode($nodeName, $node); + } + + if ($sourceContext = $block->getSourceContext()) { + $deferredBlock->setSourceContext($sourceContext); + } + + return $deferredBlock; + } +} diff --git a/system/templates/default.html.twig b/system/templates/default.html.twig new file mode 100644 index 0000000..f18206b --- /dev/null +++ b/system/templates/default.html.twig @@ -0,0 +1,4 @@ +{# Default output if no theme #} +

    ERROR: {{ page.template() ~'.'~ page.templateFormat() ~".twig" }} template not found for page: {{ page.route() }}

    +

    {{ page.title() }}

    +{{ page.content()|raw }} diff --git a/system/templates/external.html.twig b/system/templates/external.html.twig new file mode 100644 index 0000000..3fa3508 --- /dev/null +++ b/system/templates/external.html.twig @@ -0,0 +1 @@ +{# Default external template #} diff --git a/system/templates/flex/404.html.twig b/system/templates/flex/404.html.twig new file mode 100644 index 0000000..adf4f65 --- /dev/null +++ b/system/templates/flex/404.html.twig @@ -0,0 +1,4 @@ +{% set item = collection ?? object %} +{% set type = collection ? 'collection' : 'object' %} + +ERROR: Layout '{{ layout }}' for flex {{ type }} '{{ item.flexType() }}' was not found. \ No newline at end of file diff --git a/system/templates/flex/_default/collection/debug.html.twig b/system/templates/flex/_default/collection/debug.html.twig new file mode 100644 index 0000000..5a37835 --- /dev/null +++ b/system/templates/flex/_default/collection/debug.html.twig @@ -0,0 +1,5 @@ +

    {{ directory.getTitle() }} debug dump

    + +{% for object in collection %} + {% render object layout: layout %} +{% endfor %} diff --git a/system/templates/flex/_default/object/debug.html.twig b/system/templates/flex/_default/object/debug.html.twig new file mode 100644 index 0000000..dc961cd --- /dev/null +++ b/system/templates/flex/_default/object/debug.html.twig @@ -0,0 +1,4 @@ +
    +

    {{ object.key }}

    +
    {{ object.jsonSerialize()|yaml_encode }}
    +
    \ No newline at end of file diff --git a/system/templates/modular/default.html.twig b/system/templates/modular/default.html.twig new file mode 100644 index 0000000..f18206b --- /dev/null +++ b/system/templates/modular/default.html.twig @@ -0,0 +1,4 @@ +{# Default output if no theme #} +

    ERROR: {{ page.template() ~'.'~ page.templateFormat() ~".twig" }} template not found for page: {{ page.route() }}

    +

    {{ page.title() }}

    +{{ page.content()|raw }} diff --git a/system/templates/partials/messages.html.twig b/system/templates/partials/messages.html.twig new file mode 100644 index 0000000..261b3fc --- /dev/null +++ b/system/templates/partials/messages.html.twig @@ -0,0 +1,14 @@ +{% set status_mapping = {'info':'green', 'error': 'red', 'warning': 'yellow'} %} + +{% if grav.messages.all %} +
    + {% for message in grav.messages.fetch %} + + {% set scope = message.scope|e %} + {% set color = status_mapping[scope] %} + +

    {{ message.message|raw }}

    + + {% endfor %} +
    +{% endif %} diff --git a/system/templates/partials/metadata.html.twig b/system/templates/partials/metadata.html.twig new file mode 100644 index 0000000..fcf1217 --- /dev/null +++ b/system/templates/partials/metadata.html.twig @@ -0,0 +1,3 @@ +{% for meta in page.metadata %} + +{% endfor %} diff --git a/tests/_bootstrap.php b/tests/_bootstrap.php new file mode 100644 index 0000000..1c0ffd9 --- /dev/null +++ b/tests/_bootstrap.php @@ -0,0 +1,35 @@ +init(); + + foreach (array_keys($grav['setup']->getStreams()) as $stream) { + @stream_wrapper_unregister($stream); + } + + $grav['streams']; + + $grav['uri']->init(); + $grav['debugger']->init(); + $grav['assets']->init(); + + $grav['config']->set('system.cache.enabled', false); + $grav['locator']->addPath('tests', '', 'tests', false); + + return $grav; +}; + +Fixtures::add('grav', $grav); + +$fake = Factory::create(); +Fixtures::add('fake', $fake); + diff --git a/tests/_support/AcceptanceTester.php b/tests/_support/AcceptanceTester.php new file mode 100644 index 0000000..4c7dcbb --- /dev/null +++ b/tests/_support/AcceptanceTester.php @@ -0,0 +1,26 @@ +t!fmROooqL-Tz8o|Tx<=pzP32bY^zA}n1m`*SZ zayH~~sh*$2f56|-lb6+ai;Tk!uM0i7lF_H0{5Zi`GEH@D+5JM9lSj1nKe)T&;m-;` z_diSd=LVjNxEHYH)e#|P({1W0Z@V{d{K+CJxk<%)f5U2*^1@Eu4L`D?f>!k1m^N>z z$H%+^=cVf}u-|r`eeuO5&XA|aU$*!&muD*Wsr@eOopdYsS7GFfT{%^Z_bbv2jV|)7 zpBi*{E6Yu+0_pvPdZ<#k#~Xw_4`vsh3VLn7h~tUHZ`TtSwkz_?3jAU7@a?opiQms8cudkC$rRsg7umFT zp>6NVpdI;LvqNOg#<43ZIh=ofz(-?G#k;)AyK*nEsWc}4E&N*MwLid{oujB&wp)>r zf#C)a2Y53wi83RC8d;8ufd>}w4oezAOyq#)fCqelH!B-RmJtYDfOIC%Tm}XJO6sxU literal 0 HcmV?d00001 diff --git a/tests/fake/nested-site/user/pages/01.item1/home-cache-image.jpg b/tests/fake/nested-site/user/pages/01.item1/home-cache-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3170ac497409e7c7923d25e3eae4b9e4a920b211 GIT binary patch literal 156699 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�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

    xa_u8c)hsQoXBakm44Rj@Hu~WCdpmw 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�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

    xa_u8c)hsQoXBakm44Rj@Hu~WCdpmw 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�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

    xa_u8c)hsQoXBakm44Rj@Hu~WCdpmw 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 literal 0 HcmV?d00001 diff --git a/tests/fake/nested-site/user/pages/02.item2/02.item2-2/sample-image.jpg b/tests/fake/nested-site/user/pages/02.item2/02.item2-2/sample-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3170ac497409e7c7923d25e3eae4b9e4a920b211 GIT binary patch literal 156699 zcmeFY2UJs8*D!pO5K8D8AR;9cA#{SE(gR495;_7ZA+*ptih~3Ykd6q7s2J%eAShJ_ zm2N{30THo)(ghVMf`Z=(Iy26^GtWHpKI{9|zt;bR6>{%A=j^l3K4nx_u^zzze%OeJ2>HN`M!9c| zavP2EuzQGma5(*0aKIswZ|EU_ySqC!Bs75RgAMcw2n~?^_QWF~ z7)$a$gbfRE$5Q;szF}?@e@_6_&(A%WOduUo)%LKYJ=4SxH< z7f$;|K8OK=5Wid_zLE0JR`+MmY|V}JEOzTd5gUK1e4CZNdVg?D0rR+n(bx@&`GawEN_m2NSJ3wx96ANQ;fn>&{J{tX z@Pt0b6Z8)Mbp&|>3pa2d3ferr_xWvo-^PA}LC2a-Td(i42ZB*#a38h-gHG??)5#6& z82$5g1RTWB&_Fc>1^*Cvl3ReQJDrCV4w3>D@bZcZkcRfbKoZ%Xn2yL>>frvhG8RaHSzNkK_T4)l-EORlpyM0*?e5!rz7#rCHD8jyr>5vZIX8C?QkZY3sMZh2|Ah#{;=VQfPVh-- zDA2L?rF@@pg{5edC@x|!e# z#4|QD02mKg+z+DVugoJUU}?B10E5Gm624(&1iQNdiAy&@LcJ&<^wteqBp}oaOcF>T zhlT+=274$VjBcK|$^VG=@9_Hr?s}w9ch7*}Lkb(`^9Op`279{ulMiX(e}weQ!#@Dg z2@CZK2o50zgiz>2_;>g7kHh`|Rc~X-zmvmm8=FCw_djCwz2hH-TL**%limMaT)y}I z0}MS%=%IfX!baaejJFO2=fAN&3RjCELG;5RK! z^8J1^DefUZ$j)Ch)~_n!&-nfKV)|F*_ygPhg+~AVz;AcI%=qI9{u<5yNT!m=ZXxdO zzZ0k%Pd02lU=opk6sF%gtLX!=KtDNQA;064KX% zH^33EsHCi+@TW~1oAF!A-rol8+_CG&F+a5YHpUhN9rWm-1OFdw|6h#BU&Y&>`zHU& zG~68!;qOaw!}`&kvv0=B-XV~<}@BiD5{Q_z?kYk~g$R8Bl-@y89 zz<+v}fxrLfXZe>vz;{C%+^U%cxKTU~{VMM03i*HH`G<~wf!{ZA2%LKmGEn$6X7Eib z)7|R-n;$y;|F3`kJIOyH@qfwnUvm8;3jCwa|2m-SgFFQ?{}YP+byV+fgN6cde~9-- zV*i$%6M%>O4K|@rdaKpZ~L&6XQLxLsJSNZ(|gCiJ_j7-cdC|1y+jthdr5C}K} z0*RzcNLUnThZwk#n?&$BjGHY$s3Mez;^4GWJrcqlUNXnaEAv83dz z6WKW@PvxG@D<~`~E-5W5zj)9HWtOnYh+{w60Y||{d-+iio^bTtKuo=$7c@?7bWKUcL8CDe+Yo(H z&hqe>1SFpehUUcdnt-JpM#52gn{qR)Ft9uq`R)3*@FuT0vi;%KJRL>wpx#`jW-O-( z!qUQ#%NbAj(pD$pEEV6HfTuv&ts?|)bettXvK!ButP{lKND7w|Nl>Nf_V97^u^&?- z!U*w&L^dLSJmN-d9s?R8@}Urx6sY!=dJjB=(1vD7N%v~(m?6}m7%{Z1yfOQdlI>Yp zlU;B$l0gAtN|!IQmJOmgrWi#bY4YgSwb&S+KpIyqvX$ESvn#b4MnFPBPie>wggqNk zTn-k9DJ0sn1rb=2Atal;93)^_#;98Gs$-Ub6p=|uMi4vHGf0PtSTq~n2uVYrA!Z~mR0a~8X2Rp(j=mo=Z6pUsnh~S_pQ~B0REG;sOjfkaraYjOTBSb?p z0)=$xQf7Zf!c{w{yHYn%8w+9_@Uw)MnYC0CSUiLVRILzHBWmhWITonw%$RUm6rvf5 zr#Z$WP>?ziHUOSOMN+oitK3(u>?U z^wGB7+*WF2%0TA1*4)-iFZ3-bsQDAkbpHaI`YWQKUNZzm(2!qnT#chK@o>J&%D!-G z7=_aXeJ7kZvkmQx?0_|f5Sn#kQcU5QC>(p6Ko1;juoU%XybKg#3dmSX#jzuLbK^Ti zrUk&Bv%a7XS^_k)S!f-IY9hQxqBYYQhpB~JV-9T@tW0gqW!zOHi1NXowX+SopiZi2*8arn6KcjR;?& zk&JXqHQH`Enj**3;H}nNM7$UD4jd*waAVyQpcp=FjLna*btQu9T!*v|48r_oP-@F z@}gi!Y>y)Yx>LZano%mbbu=a=8A%G7F+wor&S8o~yZ}>Ky6{rLpg5^bL&g@_wo*|H5;#G?0C8wGPGu;U2B(senHnugNC?fY71O~^7Z>Em%Fa7DC=d|} zGN7T6ZFPuExplmmtuYRIy)g@XXegVC?*?qqK5M#T^ZR0##@fW7!`7?`I2ewrHbFQI zIy!6WqA!jWjY;%&Y0a#H$Hq0o*^byalL&Qe{L1XR!K{sqMc_yx8WT^@QNa@&)O#Qt zSRsErtjE+ch?TI&X4&WUXw>LvOky1zwqgyAMNR^` zUBO!~9EA)-vn$e&g`_YPtcC!l04rIf9gYVpdcudMN>OQ%L|88sB@17qaS_=?f<6)9 zM3E!e$ao-!6oF6~qN2z;1lCVIED1P9c^vB?INEhsz9A}`HDHCz4tQ=NvY_P+=M|pk z+l%xD3{cvR;jBegC-(LhXuRCrwSGGGNwBclTue&f&pLetAX_Lmg zSE`p(FI?b6IlPjR9JH$5p)#~9@ts?`_VTd8$5IbIJFMEt{L(WsKYiAU&f#U>f7^5E zMxOTAl*J>bFSv%>S^3s9tM;=jAHPB{WiNq>*t(ibD+owF@OnhVwPx5)KRb@iZq>Wgbu+A^Nzr3#Gu?d+9TQM8WRr*PCBNPHD3!d0_@ui=b%u- zi}-k{jX=qHBl>_cd68xin;Tz*_W}se6&LFwO$W%qNet~s?0PLzW&~{q$94l64h{X> zgYBwB^l0+~rlwlY>M{z0V*!LX;{^gzkq;;|Z>C8(MY9B4BP*Z?A3A}GazV4|1QAe3 zSWuU5P;Hi4fx?S$5yI*T{n$0OJr8*to{jlgRt-mUsgu7vlzcCo#wV}*?4=&#O#8Ls zJ0kuXN7rW?pRF#gQFb_%9M78T)mq+`yC`d{eY(gy=VUD6p(8}J@Fo~#WgdPM#QVBY zfXLP$*ul&1K3KIkXlm9<^o>1M;JxP5M@63|*8>u(c)^zAr`AsIHTshAvG8<~ez=mq zhIkdL!*lpy!=SHF2j{fZ{{6+Dn?n^Z%la>@!1vwyu=$qcTtiPfJIXOGWw;6x*CO`e zV&9of6}|F`yaN7~T}wA(8-et`1=pnka@Z0C7p(|6D3z^oTMu4(0_E5(#YQ{yt_?*IWl{-L0Ih znX$k&5t#$xKr1_rh~^BWA<>G}LuFA6Wv7UI4}Tt_ru**V+h(tw zw@}=?>Or+X%yrJwY*Ic(>vq?wrE8PJSikSqxd+i`?{GdD$vxNYKVL0)v?HaTZp3{o8q)7P<71t5qtCx?OT_d0Ze7p!C*Rbcl=Aj2D6DZ( zRX!s#zrvPq3`<7Er*3a6>)aEgn*b&|Z?kUu2!?O!J)5^qW&~pPf!Z zQ9dm8B@G_Qsh@cZ8!aDoM#Z~*$bNLOwfhw5hGpuTb)GX_)^G1QNNJVbIF)I}oow@R zYt)-i2I1CIOKF;U?y_vFMt5$!PdoQYX)1yxK=RA`ob=As&CAgO8O1&VVG_+xF6ZWR zl-e!}q|KgtaX!@VGWOs$;nwF}Wtz;(JonV*%L_?wHP7qaxJ;`VVR=~z#ese1h#@cy z7a47^z?rxU$i4?`d}63geo|k{lrzUnGK`HGd_Ctv+u8;i{=UinJpI zQC_S}k&txDMbW$Hpf=st4UL`CV8xyB^ECOF_{2+rW_=(@(x zXil2CG1WjDLr2gE&Q|I?XC*9L*hG_{M&z55!?J}C+JSwFmx_-s;!liZKs!VL&EErG zB}z!5V^S=1;$bw7^RJC`2(!aDo;XawGP`abjKVQu28>sr3W13wL02xd;M-Wl3-m`f zbnCFC%PqXRd~0y~Qgh3r@Dn%-&kMFll>zlWcrsUZ+E@VYzp9esOMT%UIH?ol@7f z_wA2J&z_$W{TcgSd%MFuVe2a@>Aalbj)!ySUe8a&JA{)m?I*p3)Jola1mv{FRr<@) z%_?@S^9p#>m8M_o-(u4i+>9E1dS*`T>uq@i=tE-k_;X17JKQ1+feR0u{vMvxOpWlho_I&PUd+i{qdBwt+~tv z?=2a+2P=fG=au@Un1l@5Y`2-S5Xdca7j+Nh&^M->96bZs+}$Q|AYrUfaMxnRkm)1M zJy%lv%pbpPsF8gX&AAH~WwWGQ=d^wI<44oUi%X^LNCYo+Ga4f2A^PwHCS@wHsoT)E z@J7e+rlb|X=>$w^P$HU&CnyUdx5md(H>I#SLMHG@Dae`uoq#EUCF}H2 zSU!L+0ows{p#kWoJ0Q&|XLv5qP*^^UAn+&jsg8)if?GhCLD}tcBJq%dH>6{V>{5 z;;UYdryo#~!e!gD%#9_pqKwN`jMe#jaCVnywwA)iwH0@0JaWmd**i=P$SSsKcTP=(llp>Qi6<+@2#cq zI$B9@p$zAkNmvb6Q_s2u5E%aZ%Kv$yC6=WN5!QB2pk3=g@GB>LuAH5h-64-0PG~>2p@S_ zj72d@qTh;`87*vXRzyK2jB;>Yva&DCh?E{BMdgQZkf0Q^6g#uNdK_@m2-qUU5}A0B zlw|lC190eBlHskrye&q)CL|XcLZ}aN&15C8m?9Uz&Lfg_s);**J2OS7LVNSFk@FXT zkpj^jAOPtTkspcW%|#;EEQ6RVlNnmm4CJsV7sxXi!eWcN7 zI=V1@HlTUyAaEqCS?LC0VI_7@{qZDx@q!l{j+GG!TBufb%AVn<uV< zeF7c4Z|baD^Doc6?6C^CP)qVNjgHJ}*NBRe$*9Te;fZ#eFB^ z#Vwz_zJKT7(NT+>^}=Rb$)1uq*J=*tz@o#c1C^;;l4MaGUNUC31h1u}$`DGLmdWxr``mSbRb}q@I?pyFLbnI|P;7uIr%wt~M3SU$ zT%P|5sTH$m9?!m{t6+DazSk*mvvVi){D@Wm{!W7fj(glfcjRwLaNp|hmc1%Mj27E_ z&2HFYt9%m2DgA~zSMhW^cTK^X)gawszP{6B>E>F@ygtm>&rd``tLEC=3$4-%7ng`m zg*%U3l2c8)=QJa8JG%n^x?!$Q>2qjVXhFTM?f8OUo}v+vDsX(U{EEceiE5u|j;*W> zL%E&Ti+)ls_zL|zctJn@v6$1bz4e#o$UQj873;w8#=R%m9GtI8AI7(Qxs+bABW))| zyN|zqWNdR!`4=DUSy}DqTlrqge78?*VLiWWq*4?#c&J(826^1VWA{rLB=%Aycig_9 z<($XYgs3)`Uob=&NXZ%WoVyAvBm=Bofd!t1l^qjhg z7nc;x1<#KD5}sRz?C)SZMHOI;ct*9z@p>W}AV4H}gY z6EPN|c^s|c7dW4!azM?aI_4`BvSn>Vxbt;GPqf-;t%Saj{TJ*bI*ld!Ker8+uUJKb<%Xs#pe1&_)4JEC*b8qkM zOAqxtJFvWa@3FSzom=eY`u25g2MM4~Jo>0TDQI(Q3PE)v7W!K|rM z!AS_UyrUc=W)QJpnMZLp_g1@V&MvSva~i+i9u>XW(cr{%@I#(n+@oY>zr;CL8?9!G ziGe!HV&r=K9 zy-gQ&hiIv1Bl-niG?zRq@DA{k8fNAcNa@|JiLtdlucmx>x1 z)nvAIzxt^^wSYTnOIDcv@%m@&PHQDo`|ExRm73xS4++ka$|rw3k)e;_8MEzj^{mf7 zy3@7Qsf+E1ZyApTgxq@eb97qJwZ@V5vKMewj+Q9bSH7PR}y1M{tGj+{TQueZINFE^{VihZGPTy*2C ztNLXL#e?aJHk{>5k{>SnFr~E;?LyC2&e_*pxw4!5*-GVB=3OS=gom#VT{g)Vlc`u- zK5*{IXyU9jw|{5&KF5-G?zq=NDGgjbdnk$qJtEU9-E|1m32;UDAPO}x*hkD77rg|!vGiaZ>KVYs*5nz!* zz$pfnN_$r^AJkO`j0i4zcu^-A7}|+b1QdnYgbbWMW*mke{>lrc-h&S^!km+ugQWut zh&5)$NO>3sb>cQP5T=X()9Stk<_$kn;}LXx8Tm;bBNzzLj>Hf$t@u%{KmfGO7qQ;Q zv8w@f0`ycbP&hp%ko6}bN6ShNoCsnrDr_Dbp=uamnK#yV8*@0C(_4pT&Ax*-lc^4Z z=Q2eyz+;9jfz8CAaKj6OVqhv}CJ}t)r65=*g<5iSYhoni2*M23AZig$^9nAPgskE5 za}8vdy{xB8gy$c~2Sz=kt(ol!msa#`Y5JhKP|CFuKC^!7Ui3@DRoCLC?fS-Y_wQVm zX!Pxt%@ST3K0mQ{zTR<90e1N2bKx8<-Fjic_Dro$;s?(L^Y4&09NUvEoBI@U-+QW1 zKAT&qUMzz}4g8vX(2mcpX1=(QtMhymR^>8+U|oD6ZA29?b*+CRbM7eNv8bEl4}tpZo9tyX>&7s zfJu?}jm{eb#IzK=L1x!jdvelb;pIcPGPJ}sf=ya{6mO_!)1k0qHh6k%z; z>OEUQj08mPkgYK<2}LZxxcfxJPQ|3^C@9 z>Im%V&zN6UUJ|-YKGCmXs5w6A*P!En@GIoEURu2*T)D?DuQSi_>gK^fX)|AeMH*t- zt4}RiA8Nl;&S<-)V|Ph-)?h0{U?uGt%8UX9FYVSsDZjjx`wlYRR1&LN5Z z6{7IDhdN)OC#9GsO_uUqkL^8m7`Pof#@ZYAzgZ-|+h>`a-1G6GS@#RQq=)D`o6i#4 zTskBaq5RmUX;;eQ=ch0`b}4A&y(&*X)2k+?Zx+S}g;p7xSVf#@qHL??*BiufOW(iS z$fHp{%`%vwsw%1C%A1m^m!91bygkS7xq;%5oJ#4lS*CJfq^35W2ZxiF#KimWi&w}? zS7F~ga_Pf^1c){F);*Z%Hm&}%7hBb9=5Msuhs1=Y#ZmR#7H+HZ9Q_bFOItZ$T~~J_ z&w;cR&yTZtXc`rb`dFG#EV#+H@$rd)3AxKpQ})@`msv`$dsBq50`mEUVRVj+-?_~ zd&(A+ zUrr1!ulc-eo5*p=_SRQf-u%G!gjrK!R=zjqXvX=+m4^@h^o0Gup7|U1H=AbMG2GG- z@jNHEyEa5$=}frDU_4@Ga5cG&8)Zvr>`!|X7T_XcXk6j6 z9h)yl0xn~-+Ds2!1reMbbvFz3b+&>i)HE6jD`C?kc*$WQB`^Or6oaHUfb$m34Ahr! zSuL{^n|cpF$VdD-S|;_g?wN!h~t=c`Er^6#BSFDVvDAi^k{~+GKRYY4&n~-TZ&1;y4#1+j@I$) z5xaDut(>j2Cq3cHxx5;q`g()8WyLRp?oi)Eo~3ey#^zE={+1`=^KT3K{LW@^h_j(zpd<$7?@#95*29Ev7vtlE4k*Y8!Ox5A=NK*(3f zc^o?5QsekJ?5d&i{M}0O{OPetO2Qeqd!3*g}hsnzS=-ePK7{S=>=RSIp|GOS3!s{6Tn? zNtsT!!0j`v+s^LPp4rT~wm5Qgb@6%peV>tqlxuH0iyk@ey;GVbzszJryrTGeLPOrL zZ{q#sBkAsYcym;W&g+$A>7G-JmhFkr!fJ3Rd4~Ii$r`C_ujo8{=`3hycEPZ)#Q6=>nCaZ1R&z0!g zR{GPG`?9BXi)>!arE$IJcR}AWfwv^XTV7B>CL4)$0?uR|7NpUMFrZ5lFf?759;T5d zzYTD@4q#h|$ZVnq$MKbgqM0&L7^X7M2BI4vcujmatCeh%^Fg# z(;Q6b)qb~g%FMO&jl#vY`=mNsm%9^UHCq^K6O=3| zg9q#~bT2v09zTBeTE;adDE;Z;XfuoJw6WLQbbB-1-Iu6T94rdDRps+Ckkf<#e{J*< zjqO>US*lfIStFk8nvMxC>F=0~+!+^!HZI&tJ?J!c+(INfm3w9Le)oe@u@9 zi`wki+&g??$#_Ub^3#cn=Q~W!cQ!Nc$(Pn~)VTV%skF%d&?H;()|6=6O8$i8up^x{zEHTR!&$fLOU zX98_r2&F4!&+1}px)7gYKN5$Jr`cl~aQE!|zGz>af6GtKy02tBz2{ulS4hy|E41Cg zcshJUiYGweoJYWDEE60JiXhrEzf;x&xt$giGoo+M*Rt0=k1yHv)5mqv`dYP$3YZPQW@G#&-{7;xvDt~*Q#D3j6O)yXpd zX8)yyuMq0XDkiy6dfsyAGgEDCMewP5YX@gc2{e%t^eD0Ha(SC8=~$u$`{-P)%=k4% zc};y7iO!noUQiL-nr=hKB;Ys+Q^5aZ#L{$?ykQd20+X#UUIh zX+p9?he1XQ4vI?Pz)9vT2G%tmlxVOT+(I*sfY=zQQRu_5$AZcaHsHj^xvWXW@ty63 z5xUh8?pdlX;4Qr>vd>{(Jn;6hv`Gw@e&Klr9u^B#>?dfZ!N}5 zq0pHQvR$pzyQaWWsb=KV{xE0Li8twGR;gi|RbndRloN2EcuCiT+Lw3tmE!^Z?NPVF z8^1zs@1wikUr<*+4QTB(m7L>R*(VU?bErR6?EXVd{RWcm>%0#iFsE&g7(7ZoX0K|D z1hF^(aK`aMCKAf`^}q*Xt%cjI_g?o&`k%AnaOI8SbWCPELNq3v>N5y>>An zM?QLPADJa7`x#8n?_pgTKl8>u(ZAm^RXF69rjL~4a#OZ+ute7I?)A3%-glx6DORV5 zO)O}Mr57sPV=dx5t`S`L^O4YS32-tp_4tNtsmY-b32M(^9r^tL;+U5H!Z|?vzBqs zPN&<6?_=t9K80$>e38yRUp`@bZ}o9>^h$1x)19RlgpBz0`kbKH(qUTz`@zvAC9AQl znup$e@XuCxVGmE54|WwOu�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

    xa_u8c)hsQoXBakm44Rj@Hu~WCdpmw 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: animalgrav = Fixtures::get('grav'); + $this->directInstallCommand = new DirectInstallCommand(); + } + +} + +/** + * Why this test file is empty + * + * Wasn't able to call a symfony\console. Kept having $output problem. + * symfony console \NullOutput didn't cut it. + * + * We would also need to Mock tests since downloading packages would + * make tests slow and unreliable. But it's not worth the time ATM. + * + * Look at Gpm/InstallCommandTest.php + * + * For the full story: https://git.io/vSlI3 + */ diff --git a/tests/functional/_bootstrap.php b/tests/functional/_bootstrap.php new file mode 100644 index 0000000..8a88555 --- /dev/null +++ b/tests/functional/_bootstrap.php @@ -0,0 +1,2 @@ + $autoload)); +$grav->setup('tests'); +$grav['config']->init(); + +// Find all plugins in Grav installation and autoload their classes. + +/** @var UniformResourceLocator $locator */ +$locator = Grav::instance()['locator']; +$iterator = $locator->getIterator('plugins://'); +foreach($iterator as $directory) { + if (!$directory->isDir()) { + continue; + } + $autoloader = $directory->getPathname() . '/vendor/autoload.php'; + if (file_exists($autoloader)) { + require $autoloader; + } +} diff --git a/tests/phpstan/plugins.neon b/tests/phpstan/plugins.neon new file mode 100644 index 0000000..9b8d46c --- /dev/null +++ b/tests/phpstan/plugins.neon @@ -0,0 +1,20 @@ +includes: + #- '../../vendor/phpstan/phpstan-strict-rules/rules.neon' + - '../../vendor/phpstan/phpstan-deprecation-rules/rules.neon' +parameters: + fileExtensions: + - php + excludes_analyse: + - %currentWorkingDirectory%/user/plugins/*/vendor/* + - %currentWorkingDirectory%/user/plugins/*/tests/* + bootstrap: tests/phpstan/plugins-bootstrap.php + reportUnmatchedIgnoredErrors: true + universalObjectCratesClasses: + - Grav\Common\Config\Config + - Grav\Common\Config\Languages + - Grav\Common\Config\Setup + - Grav\Common\Data\Data + - Grav\Common\GPM\Common\Package + - Grav\Common\GPM\Local\Package + - Grav\Common\GPM\Remote\Package + - Grav\Common\Session diff --git a/tests/unit.suite.yml b/tests/unit.suite.yml new file mode 100644 index 0000000..02dc9b1 --- /dev/null +++ b/tests/unit.suite.yml @@ -0,0 +1,9 @@ +# Codeception Test Suite Configuration +# +# Suite for unit (internal) tests. + +class_name: UnitTester +modules: + enabled: + - Asserts + - \Helper\Unit \ No newline at end of file diff --git a/tests/unit/Grav/Common/AssetsTest.php b/tests/unit/Grav/Common/AssetsTest.php new file mode 100644 index 0000000..a5516a5 --- /dev/null +++ b/tests/unit/Grav/Common/AssetsTest.php @@ -0,0 +1,778 @@ +grav = $grav(); + $this->assets = $this->grav['assets']; + } + + protected function _after() + { + } + + public function testAddingAssets() + { + //test add() + $this->assets->add('test.css'); + + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL, $css); + + $array = $this->assets->getCss(); + + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type":"css", + "elements":{ + "asset":"\/test.css", + "asset_type":"css", + "order":0, + "group":"head", + "position":"pipeline", + "priority":10, + "attributes":{ + "type":"text\/css", + "rel":"stylesheet" + }, + "modified":false, + "query":"" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + $this->assets->add('test.js'); + $js = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $js); + + $array = $this->assets->getJs(); + + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type":"js", + "elements":{ + "asset":"\/test.js", + "asset_type":"js", + "order":0, + "group":"head", + "position":"pipeline", + "priority":10, + "attributes":[ + + ], + "modified":false, + "query":"" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + //test addCss(). Test adding asset to a separate group + $this->assets->reset(); + $this->assets->addCSS('test.css'); + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL, $css); + + $array = $this->assets->getCss(); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type":"css", + "elements":{ + "asset":"\/test.css", + "asset_type":"css", + "order":0, + "group":"head", + "position":"pipeline", + "priority":10, + "attributes":{ + "type":"text\/css", + "rel":"stylesheet" + }, + "modified":false, + "query":"" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + //test addCss(). Testing with remote URL + $this->assets->reset(); + $this->assets->addCSS('http://www.somesite.com/test.css'); + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL, $css); + + $array = $this->assets->getCss(); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type":"css", + "elements":{ + "asset":"http:\/\/www.somesite.com\/test.css", + "asset_type":"css", + "order":0, + "group":"head", + "position":"pipeline", + "priority":10, + "attributes":{ + "type":"text\/css", + "rel":"stylesheet" + }, + "query":"" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + //test addCss() adding asset to a separate group, and with an alternate rel attribute + $this->assets->reset(); + $this->assets->addCSS('test.css', ['group' => 'alternate', 'rel' => 'alternate']); + $css = $this->assets->css('alternate'); + $this->assertSame('' . PHP_EOL, $css); + + //test addJs() + $this->assets->reset(); + $this->assets->addJs('test.js'); + $js = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $js); + + $array = $this->assets->getJs(); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type":"js", + "elements":{ + "asset":"\/test.js", + "asset_type":"js", + "order":0, + "group":"head", + "position":"pipeline", + "priority":10, + "attributes":[], + "modified":false, + "query":"" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + //Test CSS Groups + $this->assets->reset(); + $this->assets->addCSS('test.css', ['group' => 'footer']); + $css = $this->assets->css(); + $this->assertEmpty($css); + $css = $this->assets->css('footer'); + $this->assertSame('' . PHP_EOL, $css); + + $array = $this->assets->getCss(); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type": "css", + "elements": { + "asset": "/test.css", + "asset_type": "css", + "order": 0, + "group": "footer", + "position": "pipeline", + "priority": 10, + "attributes": { + "type": "text/css", + "rel": "stylesheet" + }, + "modified": false, + "query": "" + } + } + '; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + //Test JS Groups + $this->assets->reset(); + $this->assets->addJs('test.js', ['group' => 'footer']); + $js = $this->assets->js(); + $this->assertEmpty($js); + $js = $this->assets->js('footer'); + $this->assertSame('' . PHP_EOL, $js); + + $array = $this->assets->getJs(); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type": "js", + "elements": { + "asset": "/test.js", + "asset_type": "js", + "order": 0, + "group": "footer", + "position": "pipeline", + "priority": 10, + "attributes": [], + "modified": false, + "query": "" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + //Test async / defer + $this->assets->reset(); + $this->assets->addJs('test.js', ['loading' => 'async']); + $js = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $js); + + $array = $this->assets->getJs(); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type": "js", + "elements": { + "asset": "/test.js", + "asset_type": "js", + "order": 0, + "group": "head", + "position": "pipeline", + "priority": 10, + "attributes": { + "loading": "async" + }, + "modified": false, + "query": "" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + $this->assets->reset(); + $this->assets->addJs('test.js', ['loading' => 'defer']); + $js = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $js); + + $array = $this->assets->getJs(); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type": "js", + "elements": { + "asset": "/test.js", + "asset_type": "js", + "order": 0, + "group": "head", + "position": "pipeline", + "priority": 10, + "attributes": { + "loading": "defer" + }, + "modified": false, + "query": "" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + //Test inline + $this->assets->reset(); + $this->assets->setJsPipeline(true); + $this->assets->addJs('/system/assets/jquery/jquery-3.x.min.js'); + $js = $this->assets->js('head', ['loading' => 'inline']); + $this->assertContains('"jquery",[],function()', $js); + + $this->assets->reset(); + $this->assets->setCssPipeline(true); + $this->assets->addCss('/system/assets/debugger.css'); + $css = $this->assets->css('head', ['loading' => 'inline']); + $this->assertContains('div.phpdebugbar', $css); + + $this->assets->reset(); + $this->assets->setCssPipeline(true); + $this->assets->addCss('https://fonts.googleapis.com/css?family=Roboto'); + $css = $this->assets->css('head', ['loading' => 'inline']); + $this->assertContains('font-family:\'Roboto\';', $css); + + //Test adding media queries + $this->assets->reset(); + $this->assets->add('test.css', ['media' => 'only screen and (min-width: 640px)']); + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL, $css); + + + } + + public function testAddingAssetPropertiesWithArray() + { + //Test adding assets with object to define properties + $this->assets->reset(); + $this->assets->addJs('test.js', ['loading' => 'async']); + $js = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $js); + $this->assets->reset(); + + } + + public function testAddingJSAssetPropertiesWithArrayFromCollection() + { + //Test adding properties with array + $this->assets->reset(); + $this->assets->addJs('jquery', ['loading' => 'async']); + $js = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $js); + + //Test priority too + $this->assets->reset(); + $this->assets->addJs('jquery', ['loading' => 'async', 'priority' => 1]); + $this->assets->addJs('test.js', ['loading' => 'async', 'priority' => 2]); + $js = $this->assets->js(); + $this->assertSame('' . PHP_EOL . + '' . PHP_EOL, $js); + + //Test multiple groups + $this->assets->reset(); + $this->assets->addJs('jquery', ['loading' => 'async', 'priority' => 1, 'group' => 'footer']); + $this->assets->addJs('test.js', ['loading' => 'async', 'priority' => 2]); + $js = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $js); + $js = $this->assets->js('footer'); + $this->assertSame('' . PHP_EOL, $js); + + //Test adding array of assets + //Test priority too + $this->assets->reset(); + $this->assets->addJs(['jquery', 'test.js'], ['loading' => 'async']); + $js = $this->assets->js(); + + $this->assertSame('' . PHP_EOL . + '' . PHP_EOL, $js); + } + + public function testAddingLegacyFormat() + { + // regular CSS add + //test addCss(). Test adding asset to a separate group + $this->assets->reset(); + $this->assets->addCSS('test.css', 15, true, 'bottom', 'async'); + $css = $this->assets->css('bottom'); + $this->assertSame('' . PHP_EOL, $css); + + $array = $this->assets->getCss(); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type":"css", + "elements":{ + "asset":"\/test.css", + "asset_type":"css", + "order":0, + "group":"bottom", + "position":"pipeline", + "priority":15, + "attributes":{ + "type":"text\/css", + "rel":"stylesheet", + "loading":"async" + }, + "modified":false, + "query":"" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + $this->assets->reset(); + $this->assets->addJs('test.js', 15, false, 'defer', 'bottom'); + $js = $this->assets->js('bottom'); + $this->assertSame('' . PHP_EOL, $js); + + $array = $this->assets->getJs(); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type": "js", + "elements": { + "asset": "/test.js", + "asset_type": "js", + "order": 0, + "group": "bottom", + "position": "after", + "priority": 15, + "attributes": { + "loading": "defer" + }, + "modified": false, + "query": "" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + + $this->assets->reset(); + $this->assets->addInlineCss('body { color: black }', 15, 'bottom'); + $css = $this->assets->css('bottom'); + $this->assertSame('' . PHP_EOL, $css); + + $this->assets->reset(); + $this->assets->addInlineJs('alert("test")', 15, 'bottom', ['id' => 'foo']); + $js = $this->assets->js('bottom'); + $this->assertSame('' . PHP_EOL, $js); + + } + + public function testAddingCSSAssetPropertiesWithArrayFromCollection() + { + $this->assets->registerCollection('test', ['/system/assets/whoops.css']); + + //Test priority too + $this->assets->reset(); + $this->assets->addCss('test', ['priority' => 1]); + $this->assets->addCss('test.css', ['priority' => 2]); + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL . + '' . PHP_EOL, $css); + + //Test multiple groups + $this->assets->reset(); + $this->assets->addCss('test', ['priority' => 1, 'group' => 'footer']); + $this->assets->addCss('test.css', ['priority' => 2]); + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL, $css); + $css = $this->assets->css('footer'); + $this->assertSame('' . PHP_EOL, $css); + + //Test adding array of assets + //Test priority too + $this->assets->reset(); + $this->assets->addCss(['test', 'test.css'], ['loading' => 'async']); + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL . + '' . PHP_EOL, $css); + } + + public function testPriorityOfAssets() + { + $this->assets->reset(); + $this->assets->add('test.css'); + $this->assets->add('test-after.css'); + + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL . + '' . PHP_EOL, $css); + + //---------------- + $this->assets->reset(); + $this->assets->add('test-after.css', 1); + $this->assets->add('test.css', 2); + + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL . + '' . PHP_EOL, $css); + + //---------------- + $this->assets->reset(); + $this->assets->add('test-after.css', 1); + $this->assets->add('test.css', 2); + $this->assets->add('test-before.css', 3); + + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL . + '' . PHP_EOL . + '' . PHP_EOL, $css); + } + + public function testPipeline() + { + $this->assets->reset(); + + //File not existing. Pipeline searches for that file without reaching it. Output is empty. + $this->assets->add('test.css', null, true); + $this->assets->setCssPipeline(true); + $css = $this->assets->css(); + $this->assertRegExp('##', $css); + + //Add a core Grav CSS file, which is found. Pipeline will now return a file + $this->assets->add('/system/assets/debugger.css', null, true); + $css = $this->assets->css(); + $this->assertRegExp('##', $css); + } + + public function testPipelineWithTimestamp() + { + $this->assets->reset(); + $this->assets->setTimestamp('foo'); + $this->assets->setCssPipeline(true); + + //Add a core Grav CSS file, which is found. Pipeline will now return a file + $this->assets->add('/system/assets/debugger.css', null, true); + $css = $this->assets->css(); + $this->assertRegExp('##', $css); + } + + public function testInline() + { + $this->assets->reset(); + + //File not existing. Pipeline searches for that file without reaching it. Output is empty. + $this->assets->add('test.css', ['loading' => 'inline']); + $css = $this->assets->css(); + $this->assertSame("\n", $css); + + $this->assets->reset(); + //Add a core Grav CSS file, which is found. Pipeline will now return its content. + $this->assets->addCss('https://fonts.googleapis.com/css?family=Roboto', ['loading' => 'inline']); + $this->assets->addCss('/system/assets/debugger.css', ['loading' => 'inline']); + $css = $this->assets->css(); + $this->assertContains('font-family: \'Roboto\';', $css); + $this->assertContains('div.phpdebugbar-header', $css); + } + + public function testInlinePipeline() + { + $this->assets->reset(); + $this->assets->setCssPipeline(true); + + //File not existing. Pipeline searches for that file without reaching it. Output is empty. + $this->assets->add('test.css'); + $css = $this->assets->css('head', ['loading' => 'inline']); + $this->assertSame("\n", $css); + + //Add a core Grav CSS file, which is found. Pipeline will now return its content. + $this->assets->addCss('https://fonts.googleapis.com/css?family=Roboto', null, true); + $this->assets->add('/system/assets/debugger.css', null, true); + $css = $this->assets->css('head', ['loading' => 'inline']); + $this->assertContains('font-family:\'Roboto\';', $css); + $this->assertContains('div.phpdebugbar', $css); + } + + public function testAddAsyncJs() + { + $this->assets->reset(); + $this->assets->addAsyncJs('jquery'); + $js = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $js); + } + + public function testAddDeferJs() + { + $this->assets->reset(); + $this->assets->addDeferJs('jquery'); + $js = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $js); + } + + public function testTimestamps() + { + // local CSS nothing extra + $this->assets->reset(); + $this->assets->setTimestamp('foo'); + $this->assets->addCSS('test.css'); + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL, $css); + + // local CSS already with param + $this->assets->reset(); + $this->assets->setTimestamp('foo'); + $this->assets->addCSS('test.css?bar'); + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL, $css); + + // external CSS already + $this->assets->reset(); + $this->assets->setTimestamp('foo'); + $this->assets->addCSS('http://somesite.com/test.css'); + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL, $css); + + // external CSS already with param + $this->assets->reset(); + $this->assets->setTimestamp('foo'); + $this->assets->addCSS('http://somesite.com/test.css?bar'); + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL, $css); + + // local JS nothing extra + $this->assets->reset(); + $this->assets->setTimestamp('foo'); + $this->assets->addJs('test.js'); + $css = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $css); + + // local JS already with param + $this->assets->reset(); + $this->assets->setTimestamp('foo'); + $this->assets->addJs('test.js?bar'); + $css = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $css); + + // external JS already + $this->assets->reset(); + $this->assets->setTimestamp('foo'); + $this->assets->addJs('http://somesite.com/test.js'); + $css = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $css); + + // external JS already with param + $this->assets->reset(); + $this->assets->setTimestamp('foo'); + $this->assets->addJs('http://somesite.com/test.js?bar'); + $css = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $css); + } + + public function testAddInlineCss() + { + $this->assets->reset(); + $this->assets->addInlineCss('body { color: black }'); + $css = $this->assets->css(); + $this->assertSame('' . PHP_EOL, $css); + } + + public function testAddInlineJs() + { + $this->assets->reset(); + $this->assets->addInlineJs('alert("test")'); + $js = $this->assets->js(); + $this->assertSame('' . PHP_EOL, $js); + } + + public function testGetCollections() + { + $this->assertInternalType('array', $this->assets->getCollections()); + $this->assertContains('jquery', array_keys($this->assets->getCollections())); + $this->assertContains('system://assets/jquery/jquery-2.x.min.js', $this->assets->getCollections()); + } + + public function testExists() + { + $this->assertTrue($this->assets->exists('jquery')); + $this->assertFalse($this->assets->exists('another-unexisting-library')); + } + + public function testRegisterCollection() + { + $this->assets->registerCollection('debugger', ['/system/assets/debugger.css']); + $this->assertTrue($this->assets->exists('debugger')); + $this->assertContains('debugger', array_keys($this->assets->getCollections())); + } + + public function testReset() + { + $this->assets->addInlineJs('alert("test")'); + $this->assets->reset(); + $this->assertCount(0, (array) $this->assets->getJs()); + + $this->assets->addAsyncJs('jquery'); + $this->assets->reset(); + $this->assertCount(0, (array) $this->assets->getJs()); + + $this->assets->addInlineCss('body { color: black }'); + $this->assets->reset(); + $this->assertCount(0, (array) $this->assets->getCss()); + + $this->assets->add('/system/assets/debugger.css', null, true); + $this->assets->reset(); + $this->assertCount(0, (array) $this->assets->getCss()); + } + + public function testResetJs() + { + $this->assets->addInlineJs('alert("test")'); + $this->assets->resetJs(); + $this->assertCount(0, (array) $this->assets->getJs()); + + $this->assets->addAsyncJs('jquery'); + $this->assets->resetJs(); + $this->assertCount(0, (array) $this->assets->getJs()); + } + + public function testResetCss() + { + $this->assets->addInlineCss('body { color: black }'); + $this->assets->resetCss(); + $this->assertCount(0, (array) $this->assets->getCss()); + + $this->assets->add('/system/assets/debugger.css', null, true); + $this->assets->resetCss(); + $this->assertCount(0, (array) $this->assets->getCss()); + } + + public function testAddDirCss() + { + $this->assets->addDirCss('/system'); + + $this->assertInternalType('array', $this->assets->getCss()); + $this->assertGreaterThan(0, (array) $this->assets->getCss()); + $this->assertInternalType('array', $this->assets->getJs()); + $this->assertCount(0, (array) $this->assets->getJs()); + + $this->assets->reset(); + $this->assets->addDirCss('/system/assets'); + + $this->assertInternalType('array', $this->assets->getCss()); + $this->assertGreaterThan(0, (array) $this->assets->getCss()); + $this->assertInternalType('array', $this->assets->getJs()); + $this->assertCount(0, (array) $this->assets->getJs()); + + $this->assets->reset(); + $this->assets->addDirJs('/system'); + + $this->assertInternalType('array', $this->assets->getCss()); + $this->assertCount(0, (array) $this->assets->getCss()); + $this->assertInternalType('array', $this->assets->getJs()); + $this->assertGreaterThan(0, (array) $this->assets->getJs()); + + $this->assets->reset(); + $this->assets->addDirJs('/system/assets'); + + $this->assertInternalType('array', $this->assets->getCss()); + $this->assertCount(0, (array) $this->assets->getCss()); + $this->assertInternalType('array', $this->assets->getJs()); + $this->assertGreaterThan(0, (array) $this->assets->getJs()); + + $this->assets->reset(); + $this->assets->addDir('/system/assets'); + + $this->assertInternalType('array', $this->assets->getCss()); + $this->assertGreaterThan(0, (array) $this->assets->getCss()); + $this->assertInternalType('array', $this->assets->getJs()); + $this->assertGreaterThan(0, (array) $this->assets->getJs()); + + //Use streams + $this->assets->reset(); + $this->assets->addDir('system://assets'); + + $this->assertInternalType('array', $this->assets->getCss()); + $this->assertGreaterThan(0, (array) $this->assets->getCss()); + $this->assertInternalType('array', $this->assets->getJs()); + $this->assertGreaterThan(0, (array) $this->assets->getJs()); + + } +} diff --git a/tests/unit/Grav/Common/BrowserTest.php b/tests/unit/Grav/Common/BrowserTest.php new file mode 100644 index 0000000..6e63738 --- /dev/null +++ b/tests/unit/Grav/Common/BrowserTest.php @@ -0,0 +1,48 @@ +grav = $grav(); + } + + protected function _after() + { + } + + public function testGetBrowser() + { /* Already covered by PhpUserAgent tests */ + } + + public function testGetPlatform() + { /* Already covered by PhpUserAgent tests */ + } + + public function testGetLongVersion() + { /* Already covered by PhpUserAgent tests */ + } + + public function testGetVersion() + { /* Already covered by PhpUserAgent tests */ + } + + public function testIsHuman() + { + //Already Partially covered by PhpUserAgent tests + + //Make sure it recognizes the test as not human + $this->assertFalse($this->grav['browser']->isHuman()); + } +} + diff --git a/tests/unit/Grav/Common/ComposerTest.php b/tests/unit/Grav/Common/ComposerTest.php new file mode 100644 index 0000000..243a888 --- /dev/null +++ b/tests/unit/Grav/Common/ComposerTest.php @@ -0,0 +1,32 @@ +assertInternalType('string', $composerLocation); + $this->assertSame('/', $composerLocation[0]); + } + + public function testGetComposerExecutor() + { + $composerExecutor = Composer::getComposerExecutor(); + $this->assertInternalType('string', $composerExecutor); + $this->assertSame('/', $composerExecutor[0]); + $this->assertNotNull(strstr($composerExecutor, 'php')); + $this->assertNotNull(strstr($composerExecutor, 'composer')); + } + +} diff --git a/tests/unit/Grav/Common/GPM/GPMTest.php b/tests/unit/Grav/Common/GPM/GPMTest.php new file mode 100644 index 0000000..7e1c03e --- /dev/null +++ b/tests/unit/Grav/Common/GPM/GPMTest.php @@ -0,0 +1,323 @@ +data[$packageName])) { + return $this->data[$packageName]; + } + + } + + public function findPackages($searches = []) + { + return $this->data; + } +} + +/** + * Class InstallCommandTest + */ +class GpmTest extends \Codeception\TestCase\Test +{ + /** @var Grav $grav */ + protected $grav; + + /** @var GpmStub */ + protected $gpm; + + protected function _before() + { + $this->grav = Fixtures::get('grav'); + $this->gpm = new GpmStub(); + } + + protected function _after() + { + } + + public function testCalculateMergedDependenciesOfPackages() + { + ////////////////////////////////////////////////////////////////////////////////////////// + // First working example + ////////////////////////////////////////////////////////////////////////////////////////// + $this->gpm->data = [ + 'admin' => (object)[ + 'dependencies' => [ + ["name" => "grav", "version" => ">=1.0.10"], + ["name" => "form", "version" => "~2.0"], + ["name" => "login", "version" => ">=2.0"], + ["name" => "errors", "version" => "*"], + ["name" => "problems"], + ] + ], + 'test' => (object)[ + 'dependencies' => [ + ["name" => "errors", "version" => ">=1.0"] + ] + ], + 'grav', + 'form' => (object)[ + 'dependencies' => [ + ["name" => "errors", "version" => ">=3.2"] + ] + ] + + + ]; + + $packages = ['admin', 'test']; + + $dependencies = $this->gpm->calculateMergedDependenciesOfPackages($packages); + + $this->assertInternalType('array', $dependencies); + $this->assertCount(5, $dependencies); + + $this->assertSame('>=1.0.10', $dependencies['grav']); + $this->assertArrayHasKey('errors', $dependencies); + $this->assertArrayHasKey('problems', $dependencies); + + ////////////////////////////////////////////////////////////////////////////////////////// + // Second working example + ////////////////////////////////////////////////////////////////////////////////////////// + $packages = ['admin', 'form']; + + $dependencies = $this->gpm->calculateMergedDependenciesOfPackages($packages); + $this->assertInternalType('array', $dependencies); + $this->assertCount(5, $dependencies); + $this->assertSame('>=3.2', $dependencies['errors']); + + ////////////////////////////////////////////////////////////////////////////////////////// + // Third working example + ////////////////////////////////////////////////////////////////////////////////////////// + $this->gpm->data = [ + + 'admin' => (object)[ + 'dependencies' => [ + ["name" => "errors", "version" => ">=4.0"], + ] + ], + 'test' => (object)[ + 'dependencies' => [ + ["name" => "errors", "version" => ">=1.0"] + ] + ], + 'another' => (object)[ + 'dependencies' => [ + ["name" => "errors", "version" => ">=3.2"] + ] + ] + + ]; + + $packages = ['admin', 'test', 'another']; + + + $dependencies = $this->gpm->calculateMergedDependenciesOfPackages($packages); + $this->assertInternalType('array', $dependencies); + $this->assertCount(1, $dependencies); + $this->assertSame('>=4.0', $dependencies['errors']); + + + + ////////////////////////////////////////////////////////////////////////////////////////// + // Test alpha / beta / rc + ////////////////////////////////////////////////////////////////////////////////////////// + $this->gpm->data = [ + 'admin' => (object)[ + 'dependencies' => [ + ["name" => "package1", "version" => ">=4.0.0-rc1"], + ["name" => "package4", "version" => ">=3.2.0"], + ] + ], + 'test' => (object)[ + 'dependencies' => [ + ["name" => "package1", "version" => ">=4.0.0-rc2"], + ["name" => "package2", "version" => ">=3.2.0-alpha"], + ["name" => "package3", "version" => ">=3.2.0-alpha.2"], + ["name" => "package4", "version" => ">=3.2.0-alpha"], + ] + ], + 'another' => (object)[ + 'dependencies' => [ + ["name" => "package2", "version" => ">=3.2.0-beta.11"], + ["name" => "package3", "version" => ">=3.2.0-alpha.1"], + ["name" => "package4", "version" => ">=3.2.0-beta"], + ] + ] + ]; + + $packages = ['admin', 'test', 'another']; + + + $dependencies = $this->gpm->calculateMergedDependenciesOfPackages($packages); + $this->assertSame('>=4.0.0-rc2', $dependencies['package1']); + $this->assertSame('>=3.2.0-beta.11', $dependencies['package2']); + $this->assertSame('>=3.2.0-alpha.2', $dependencies['package3']); + $this->assertSame('>=3.2.0', $dependencies['package4']); + + + ////////////////////////////////////////////////////////////////////////////////////////// + // Raise exception if no version is specified + ////////////////////////////////////////////////////////////////////////////////////////// + $this->gpm->data = [ + + 'admin' => (object)[ + 'dependencies' => [ + ["name" => "errors", "version" => ">=4.0"], + ] + ], + 'test' => (object)[ + 'dependencies' => [ + ["name" => "errors", "version" => ">="] + ] + ], + + ]; + + $packages = ['admin', 'test']; + + try { + $this->gpm->calculateMergedDependenciesOfPackages($packages); + $this->fail("Expected Exception not thrown"); + } catch (Exception $e) { + $this->assertEquals(EXCEPTION_BAD_FORMAT, $e->getCode()); + $this->assertStringStartsWith("Bad format for version of dependency", $e->getMessage()); + } + + ////////////////////////////////////////////////////////////////////////////////////////// + // Raise exception if incompatible versions are specified + ////////////////////////////////////////////////////////////////////////////////////////// + $this->gpm->data = [ + 'admin' => (object)[ + 'dependencies' => [ + ["name" => "errors", "version" => "~4.0"], + ] + ], + 'test' => (object)[ + 'dependencies' => [ + ["name" => "errors", "version" => "~3.0"] + ] + ], + ]; + + $packages = ['admin', 'test']; + + try { + $this->gpm->calculateMergedDependenciesOfPackages($packages); + $this->fail("Expected Exception not thrown"); + } catch (Exception $e) { + $this->assertEquals(EXCEPTION_INCOMPATIBLE_VERSIONS, $e->getCode()); + $this->assertStringEndsWith("required in two incompatible versions", $e->getMessage()); + } + + ////////////////////////////////////////////////////////////////////////////////////////// + // Test dependencies of dependencies + ////////////////////////////////////////////////////////////////////////////////////////// + $this->gpm->data = [ + 'admin' => (object)[ + 'dependencies' => [ + ["name" => "grav", "version" => ">=1.0.10"], + ["name" => "form", "version" => "~2.0"], + ["name" => "login", "version" => ">=2.0"], + ["name" => "errors", "version" => "*"], + ["name" => "problems"], + ] + ], + 'login' => (object)[ + 'dependencies' => [ + ["name" => "antimatter", "version" => ">=1.0"] + ] + ], + 'grav', + 'antimatter' => (object)[ + 'dependencies' => [ + ["name" => "something", "version" => ">=3.2"] + ] + ] + + + ]; + + $packages = ['admin']; + + $dependencies = $this->gpm->calculateMergedDependenciesOfPackages($packages); + + $this->assertInternalType('array', $dependencies); + $this->assertCount(7, $dependencies); + + $this->assertSame('>=1.0.10', $dependencies['grav']); + $this->assertArrayHasKey('errors', $dependencies); + $this->assertArrayHasKey('problems', $dependencies); + $this->assertArrayHasKey('antimatter', $dependencies); + $this->assertArrayHasKey('something', $dependencies); + $this->assertSame('>=3.2', $dependencies['something']); + } + + public function testVersionFormatIsNextSignificantRelease() + { + $this->assertFalse($this->gpm->versionFormatIsNextSignificantRelease('>=1.0')); + $this->assertFalse($this->gpm->versionFormatIsNextSignificantRelease('>=2.3.4')); + $this->assertFalse($this->gpm->versionFormatIsNextSignificantRelease('>=2.3.x')); + $this->assertFalse($this->gpm->versionFormatIsNextSignificantRelease('1.0')); + $this->assertTrue($this->gpm->versionFormatIsNextSignificantRelease('~2.3.x')); + $this->assertTrue($this->gpm->versionFormatIsNextSignificantRelease('~2.0')); + } + + public function testVersionFormatIsEqualOrHigher() + { + $this->assertTrue($this->gpm->versionFormatIsEqualOrHigher('>=1.0')); + $this->assertTrue($this->gpm->versionFormatIsEqualOrHigher('>=2.3.4')); + $this->assertTrue($this->gpm->versionFormatIsEqualOrHigher('>=2.3.x')); + $this->assertFalse($this->gpm->versionFormatIsEqualOrHigher('~2.3.x')); + $this->assertFalse($this->gpm->versionFormatIsEqualOrHigher('1.0')); + } + + public function testCheckNextSignificantReleasesAreCompatible() + { + /* + * ~1.0 is equivalent to >=1.0 < 2.0.0 + * ~1.2 is equivalent to >=1.2 <2.0.0 + * ~1.2.3 is equivalent to >=1.2.3 <1.3.0 + */ + $this->assertTrue($this->gpm->checkNextSignificantReleasesAreCompatible('1.0', '1.2')); + $this->assertTrue($this->gpm->checkNextSignificantReleasesAreCompatible('1.2', '1.0')); + $this->assertTrue($this->gpm->checkNextSignificantReleasesAreCompatible('1.0', '1.0.10')); + $this->assertTrue($this->gpm->checkNextSignificantReleasesAreCompatible('1.1', '1.1.10')); + $this->assertTrue($this->gpm->checkNextSignificantReleasesAreCompatible('30.0', '30.10')); + $this->assertTrue($this->gpm->checkNextSignificantReleasesAreCompatible('1.0', '1.1.10')); + $this->assertTrue($this->gpm->checkNextSignificantReleasesAreCompatible('1.0', '1.8')); + $this->assertTrue($this->gpm->checkNextSignificantReleasesAreCompatible('1.0.1', '1.1')); + $this->assertTrue($this->gpm->checkNextSignificantReleasesAreCompatible('2.0.0-beta', '2.0')); + $this->assertTrue($this->gpm->checkNextSignificantReleasesAreCompatible('2.0.0-rc.1', '2.0')); + $this->assertTrue($this->gpm->checkNextSignificantReleasesAreCompatible('2.0', '2.0.0-alpha')); + + $this->assertFalse($this->gpm->checkNextSignificantReleasesAreCompatible('1.0', '2.2')); + $this->assertFalse($this->gpm->checkNextSignificantReleasesAreCompatible('1.0.0-beta.1', '2.0')); + $this->assertFalse($this->gpm->checkNextSignificantReleasesAreCompatible('0.9.99', '1.0.0')); + $this->assertFalse($this->gpm->checkNextSignificantReleasesAreCompatible('0.9.99', '1.0.10')); + $this->assertFalse($this->gpm->checkNextSignificantReleasesAreCompatible('0.9.99', '1.0.10.2')); + } + + + public function testCalculateVersionNumberFromDependencyVersion() + { + $this->assertSame('2.0', $this->gpm->calculateVersionNumberFromDependencyVersion('>=2.0')); + $this->assertSame('2.0.2', $this->gpm->calculateVersionNumberFromDependencyVersion('>=2.0.2')); + $this->assertSame('2.0.2', $this->gpm->calculateVersionNumberFromDependencyVersion('~2.0.2')); + $this->assertSame('1', $this->gpm->calculateVersionNumberFromDependencyVersion('~1')); + $this->assertNull($this->gpm->calculateVersionNumberFromDependencyVersion('')); + $this->assertNull($this->gpm->calculateVersionNumberFromDependencyVersion('*')); + $this->assertSame('2.0.2', $this->gpm->calculateVersionNumberFromDependencyVersion('2.0.2')); + } +} diff --git a/tests/unit/Grav/Common/Helpers/ExcerptsTest.php b/tests/unit/Grav/Common/Helpers/ExcerptsTest.php new file mode 100644 index 0000000..b57b385 --- /dev/null +++ b/tests/unit/Grav/Common/Helpers/ExcerptsTest.php @@ -0,0 +1,85 @@ +grav = $grav(); + $this->pages = $this->grav['pages']; + $this->config = $this->grav['config']; + $this->uri = $this->grav['uri']; + $this->language = $this->grav['language']; + $this->old_home = $this->config->get('system.home.alias'); + $this->config->set('system.home.alias', '/item1'); + $this->config->set('system.absolute_urls', false); + $this->config->set('system.languages.supported', []); + + unset($this->grav['language']); + $this->grav['language'] = new Language($this->grav); + + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + $locator->addPath('page', '', 'tests/fake/nested-site/user/pages', false); + $this->pages->init(); + + $defaults = [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ]; + $this->page = $this->pages->dispatch('/item2/item2-2'); + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + } + + protected function _after() + { + $this->config->set('system.home.alias', $this->old_home); + } + + + public function testProcessImageHtml() + { + $this->assertRegexp('|Sample Image|', + Excerpts::processImageHtml('Sample Image', $this->page)); + $this->assertRegexp('|Sample Image|', + Excerpts::processImageHtml('Sample Image', $this->page)); + } + +} diff --git a/tests/unit/Grav/Common/InflectorTest.php b/tests/unit/Grav/Common/InflectorTest.php new file mode 100644 index 0000000..9fad489 --- /dev/null +++ b/tests/unit/Grav/Common/InflectorTest.php @@ -0,0 +1,147 @@ +grav = $grav(); + $this->inflector = $this->grav['inflector']; + } + + protected function _after() + { + } + + public function testPluralize() + { + $this->assertSame('words', $this->inflector->pluralize('word')); + $this->assertSame('kisses', $this->inflector->pluralize('kiss')); + $this->assertSame('volcanoes', $this->inflector->pluralize('volcanoe')); + $this->assertSame('cherries', $this->inflector->pluralize('cherry')); + $this->assertSame('days', $this->inflector->pluralize('day')); + $this->assertSame('knives', $this->inflector->pluralize('knife')); + } + + public function testSingularize() + { + $this->assertSame('word', $this->inflector->singularize('words')); + $this->assertSame('kiss', $this->inflector->singularize('kisses')); + $this->assertSame('volcanoe', $this->inflector->singularize('volcanoe')); + $this->assertSame('cherry', $this->inflector->singularize('cherries')); + $this->assertSame('day', $this->inflector->singularize('days')); + $this->assertSame('knife', $this->inflector->singularize('knives')); + } + + public function testTitleize() + { + $this->assertSame('This String Is Titleized', $this->inflector->titleize('ThisStringIsTitleized')); + $this->assertSame('This String Is Titleized', $this->inflector->titleize('this string is titleized')); + $this->assertSame('This String Is Titleized', $this->inflector->titleize('this_string_is_titleized')); + $this->assertSame('This String Is Titleized', $this->inflector->titleize('this-string-is-titleized')); + + $this->assertSame('This string is titleized', $this->inflector->titleize('ThisStringIsTitleized', 'first')); + $this->assertSame('This string is titleized', $this->inflector->titleize('this string is titleized', 'first')); + $this->assertSame('This string is titleized', $this->inflector->titleize('this_string_is_titleized', 'first')); + $this->assertSame('This string is titleized', $this->inflector->titleize('this-string-is-titleized', 'first')); + } + + public function testCamelize() + { + $this->assertSame('ThisStringIsCamelized', $this->inflector->camelize('This String Is Camelized')); + $this->assertSame('ThisStringIsCamelized', $this->inflector->camelize('thisStringIsCamelized')); + $this->assertSame('ThisStringIsCamelized', $this->inflector->camelize('This_String_Is_Camelized')); + $this->assertSame('ThisStringIsCamelized', $this->inflector->camelize('this string is camelized')); + $this->assertSame('GravSPrettyCoolMy1', $this->inflector->camelize("Grav's Pretty Cool. My #1!")); + } + + public function testUnderscorize() + { + $this->assertSame('this_string_is_underscorized', $this->inflector->underscorize('This String Is Underscorized')); + $this->assertSame('this_string_is_underscorized', $this->inflector->underscorize('ThisStringIsUnderscorized')); + $this->assertSame('this_string_is_underscorized', $this->inflector->underscorize('This_String_Is_Underscorized')); + $this->assertSame('this_string_is_underscorized', $this->inflector->underscorize('This-String-Is-Underscorized')); + } + + public function testHyphenize() + { + $this->assertSame('this-string-is-hyphenized', $this->inflector->hyphenize('This String Is Hyphenized')); + $this->assertSame('this-string-is-hyphenized', $this->inflector->hyphenize('ThisStringIsHyphenized')); + $this->assertSame('this-string-is-hyphenized', $this->inflector->hyphenize('This-String-Is-Hyphenized')); + $this->assertSame('this-string-is-hyphenized', $this->inflector->hyphenize('This_String_Is_Hyphenized')); + } + + public function testHumanize() + { + //$this->assertSame('This string is humanized', $this->inflector->humanize('ThisStringIsHumanized')); + $this->assertSame('This string is humanized', $this->inflector->humanize('this_string_is_humanized')); + //$this->assertSame('This string is humanized', $this->inflector->humanize('this-string-is-humanized')); + + $this->assertSame('This String Is Humanized', $this->inflector->humanize('this_string_is_humanized', 'all')); + //$this->assertSame('This String Is Humanized', $this->inflector->humanize('this-string-is-humanized'), 'all'); + } + + public function testVariablize() + { + $this->assertSame('thisStringIsVariablized', $this->inflector->variablize('This String Is Variablized')); + $this->assertSame('thisStringIsVariablized', $this->inflector->variablize('ThisStringIsVariablized')); + $this->assertSame('thisStringIsVariablized', $this->inflector->variablize('This_String_Is_Variablized')); + $this->assertSame('thisStringIsVariablized', $this->inflector->variablize('this string is variablized')); + $this->assertSame('gravSPrettyCoolMy1', $this->inflector->variablize("Grav's Pretty Cool. My #1!")); + } + + public function testTableize() + { + $this->assertSame('people', $this->inflector->tableize('Person')); + $this->assertSame('pages', $this->inflector->tableize('Page')); + $this->assertSame('blog_pages', $this->inflector->tableize('BlogPage')); + $this->assertSame('admin_dependencies', $this->inflector->tableize('adminDependency')); + $this->assertSame('admin_dependencies', $this->inflector->tableize('admin-dependency')); + $this->assertSame('admin_dependencies', $this->inflector->tableize('admin_dependency')); + } + + public function testClassify() + { + $this->assertSame('Person', $this->inflector->classify('people')); + $this->assertSame('Page', $this->inflector->classify('pages')); + $this->assertSame('BlogPage', $this->inflector->classify('blog_pages')); + $this->assertSame('AdminDependency', $this->inflector->classify('admin_dependencies')); + } + + public function testOrdinalize() + { + $this->assertSame('1st', $this->inflector->ordinalize(1)); + $this->assertSame('2nd', $this->inflector->ordinalize(2)); + $this->assertSame('3rd', $this->inflector->ordinalize(3)); + $this->assertSame('4th', $this->inflector->ordinalize(4)); + $this->assertSame('5th', $this->inflector->ordinalize(5)); + $this->assertSame('16th', $this->inflector->ordinalize(16)); + $this->assertSame('51st', $this->inflector->ordinalize(51)); + $this->assertSame('111th', $this->inflector->ordinalize(111)); + $this->assertSame('123rd', $this->inflector->ordinalize(123)); + } + + public function testMonthize() + { + $this->assertSame(0, $this->inflector->monthize(10)); + $this->assertSame(1, $this->inflector->monthize(33)); + $this->assertSame(1, $this->inflector->monthize(41)); + $this->assertSame(11, $this->inflector->monthize(364)); + } +} + + diff --git a/tests/unit/Grav/Common/Language/LanguageCodesTest.php b/tests/unit/Grav/Common/Language/LanguageCodesTest.php new file mode 100644 index 0000000..ef6cabc --- /dev/null +++ b/tests/unit/Grav/Common/Language/LanguageCodesTest.php @@ -0,0 +1,24 @@ +assertSame('ltr', + LanguageCodes::getOrientation('en')); + $this->assertSame('rtl', + LanguageCodes::getOrientation('ar')); + $this->assertSame('rtl', + LanguageCodes::getOrientation('he')); + $this->assertTrue(LanguageCodes::isRtl('ar')); + $this->assertFalse(LanguageCodes::isRtl('fr')); + + } + +} diff --git a/tests/unit/Grav/Common/Markdown/ParsedownTest.php b/tests/unit/Grav/Common/Markdown/ParsedownTest.php new file mode 100644 index 0000000..58a1acf --- /dev/null +++ b/tests/unit/Grav/Common/Markdown/ParsedownTest.php @@ -0,0 +1,757 @@ +grav = $grav(); + $this->pages = $this->grav['pages']; + $this->config = $this->grav['config']; + $this->uri = $this->grav['uri']; + $this->language = $this->grav['language']; + $this->old_home = $this->config->get('system.home.alias'); + $this->config->set('system.home.alias', '/item1'); + $this->config->set('system.absolute_urls', false); + $this->config->set('system.languages.supported', []); + + unset($this->grav['language']); + $this->grav['language'] = new Language($this->grav); + + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + $locator->addPath('page', '', 'tests/fake/nested-site/user/pages', false); + $this->pages->init(); + + $defaults = [ + 'markdown' => [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ], + 'images' => $this->config->get('system.images', []) + ]; + $page = $this->pages->dispatch('/item2/item2-2'); + + $excerpts = new Excerpts($page, $defaults); + $this->parsedown = new Parsedown($excerpts); + } + + protected function _after() + { + $this->config->set('system.home.alias', $this->old_home); + } + + public function testImages() + { + $this->config->set('system.languages.supported', ['fr','en']); + unset($this->grav['language']); + $this->grav['language'] = new Language($this->grav); + $this->uri->initializeWithURL('http://testing.dev/fr/item2/item2-2')->init(); + + $this->assertSame('

    ', + $this->parsedown->text('![](sample-image.jpg)')); + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](cache-image.jpg?cropResize=200,200&foo)')); + + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $this->assertSame('

    ', + $this->parsedown->text('![](sample-image.jpg)')); + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](cache-image.jpg?cropResize=200,200&foo)')); + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](/home-cache-image.jpg?cache)')); + $this->assertSame('

    ', + $this->parsedown->text('![](missing-image.jpg)')); + $this->assertSame('

    ', + $this->parsedown->text('![](/home-missing-image.jpg)')); + $this->assertSame('

    ', + $this->parsedown->text('![](/home-missing-image.jpg)')); + $this->assertSame('

    ', + $this->parsedown->text('![](https://getgrav-grav.netdna-ssl.com/user/pages/media/grav-logo.svg)')); + + } + + public function testImagesSubDir() + { + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](/home-cache-image.jpg?cache)')); + $this->assertSame('

    ', + $this->parsedown->text('![](sample-image.jpg)')); + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](cache-image.jpg?cache)')); + $this->assertSame('

    ', + $this->parsedown->text('![](missing-image.jpg)')); + $this->assertSame('

    ', + $this->parsedown->text('![](/home-missing-image.jpg)')); + + } + + public function testImagesAbsoluteUrls() + { + $this->config->set('system.absolute_urls', true); + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $this->assertSame('

    ', + $this->parsedown->text('![](sample-image.jpg)')); + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](cache-image.jpg?cache)')); + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](/home-cache-image.jpg?cache)')); + $this->assertSame('

    ', + $this->parsedown->text('![](missing-image.jpg)')); + $this->assertSame('

    ', + $this->parsedown->text('![](/home-missing-image.jpg)')); + } + + public function testImagesSubDirAbsoluteUrls() + { + $this->config->set('system.absolute_urls', true); + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    ', + $this->parsedown->text('![](sample-image.jpg)')); + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](cache-image.jpg?cache)')); + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](/home-cache-image.jpg?cropResize=200,200)')); + $this->assertSame('

    ', + $this->parsedown->text('![](missing-image.jpg)')); + $this->assertSame('

    ', + $this->parsedown->text('![](/home-missing-image.jpg)')); + } + + public function testImagesAttributes() + { + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $this->assertSame('

    ', + $this->parsedown->text('![](sample-image.jpg "My Title")')); + $this->assertSame('

    ', + $this->parsedown->text('![](sample-image.jpg?classes=foo)')); + $this->assertSame('

    ', + $this->parsedown->text('![](sample-image.jpg?classes=foo,bar)')); + $this->assertSame('

    ', + $this->parsedown->text('![](sample-image.jpg?id=foo)')); + $this->assertSame('

    Alt Text

    ', + $this->parsedown->text('![Alt Text](sample-image.jpg?id=foo)')); + $this->assertSame('

    Alt Text

    ', + $this->parsedown->text('![Alt Text](sample-image.jpg?class=bar&id=foo)')); + $this->assertSame('

    Alt Text

    ', + $this->parsedown->text('![Alt Text](sample-image.jpg?class=bar&id=foo "My Title")')); + } + + + public function testRootImages() + { + $this->uri->initializeWithURL('http://testing.dev/')->init(); + + $defaults = [ + 'markdown' => [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ], + 'images' => $this->config->get('system.images', []) + ]; + $page = $this->pages->dispatch('/'); + $excerpts = new Excerpts($page, $defaults); + $this->parsedown = new Parsedown($excerpts); + + $this->assertSame('

    ', + $this->parsedown->text('![](home-sample-image.jpg)')); + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](home-cache-image.jpg?cache)')); + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](home-cache-image.jpg?cropResize=200,200&foo)')); + $this->assertSame('

    ', + $this->parsedown->text('![](/home-missing-image.jpg)')); + + $this->config->set('system.languages.supported', ['fr','en']); + unset($this->grav['language']); + $this->grav['language'] = new Language($this->grav); + $this->uri->initializeWithURL('http://testing.dev/fr/item2/item2-2')->init(); + + $this->assertSame('

    ', + $this->parsedown->text('![](home-sample-image.jpg)')); + + + } + + public function testRootImagesSubDirAbsoluteUrls() + { + $this->config->set('system.absolute_urls', true); + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    ', + $this->parsedown->text('![](sample-image.jpg)')); + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](cache-image.jpg?cache)')); + $this->assertRegexp('|

    <\/p>|', + $this->parsedown->text('![](/home-cache-image.jpg?cropResize=200,200)')); + $this->assertSame('

    ', + $this->parsedown->text('![](missing-image.jpg)')); + $this->assertSame('

    ', + $this->parsedown->text('![](/home-missing-image.jpg)')); + } + + public function testRootAbsoluteLinks() + { + $this->uri->initializeWithURL('http://testing.dev/')->init(); + + $defaults = [ + 'markdown' => [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ], + 'images' => $this->config->get('system.images', []) + ]; + $page = $this->pages->dispatch('/'); + $excerpts = new Excerpts($page, $defaults); + $this->parsedown = new Parsedown($excerpts); + + $this->assertSame('

    Down a Level

    ', + $this->parsedown->text('[Down a Level](item1-3)')); + + $this->assertSame('

    Peer Page

    ', + $this->parsedown->text('[Peer Page](../item2)')); + + $this->assertSame('

    With Query

    ', + $this->parsedown->text('[With Query](?foo=bar)')); + $this->assertSame('

    With Param

    ', + $this->parsedown->text('[With Param](/foo:bar)')); + $this->assertSame('

    With Anchor

    ', + $this->parsedown->text('[With Anchor](#foo)')); + + $this->config->set('system.languages.supported', ['fr','en']); + unset($this->grav['language']); + $this->grav['language'] = new Language($this->grav); + $this->uri->initializeWithURL('http://testing.dev/fr/item2/item2-2')->init(); + + $this->assertSame('

    Peer Page

    ', + $this->parsedown->text('[Peer Page](../item2)')); + $this->assertSame('

    Down a Level

    ', + $this->parsedown->text('[Down a Level](item1-3)')); + $this->assertSame('

    With Query

    ', + $this->parsedown->text('[With Query](?foo=bar)')); + $this->assertSame('

    With Param

    ', + $this->parsedown->text('[With Param](/foo:bar)')); + $this->assertSame('

    With Anchor

    ', + $this->parsedown->text('[With Anchor](#foo)')); + } + + + public function testAnchorLinksLangRelativeUrls() + { + $this->config->set('system.languages.supported', ['fr','en']); + unset($this->grav['language']); + $this->grav['language'] = new Language($this->grav); + $this->uri->initializeWithURL('http://testing.dev/fr/item2/item2-2')->init(); + + $this->assertSame('

    Current Anchor

    ', + $this->parsedown->text('[Current Anchor](#foo)')); + $this->assertSame('

    Root Anchor

    ', + $this->parsedown->text('[Root Anchor](/#foo)')); + $this->assertSame('

    Peer Anchor

    ', + $this->parsedown->text('[Peer Anchor](../item2-1#foo)')); + $this->assertSame('

    Peer Anchor 2

    ', + $this->parsedown->text('[Peer Anchor 2](../item2-1/#foo)')); + + } + + public function testAnchorLinksLangAbsoluteUrls() + { + $this->config->set('system.absolute_urls', true); + $this->config->set('system.languages.supported', ['fr','en']); + unset($this->grav['language']); + $this->grav['language'] = new Language($this->grav); + $this->uri->initializeWithURL('http://testing.dev/fr/item2/item2-2')->init(); + + $this->assertSame('

    Current Anchor

    ', + $this->parsedown->text('[Current Anchor](#foo)')); + $this->assertSame('

    Peer Anchor

    ', + $this->parsedown->text('[Peer Anchor](../item2-1#foo)')); + $this->assertSame('

    Peer Anchor 2

    ', + $this->parsedown->text('[Peer Anchor 2](../item2-1/#foo)')); + $this->assertSame('

    Root Anchor

    ', + $this->parsedown->text('[Root Anchor](/#foo)')); + + } + + + public function testExternalLinks() + { + $this->assertSame('

    cnn.com

    ', + $this->parsedown->text('[cnn.com](http://www.cnn.com)')); + $this->assertSame('

    google.com

    ', + $this->parsedown->text('[google.com](https://www.google.com)')); + $this->assertSame('

    complex url

    ', + $this->parsedown->text('[complex url](https://github.com/getgrav/grav/issues/new?title=[add-resource]%20New%20Plugin/Theme&body=Hello%20**There**)')); + } + + public function testExternalLinksSubDir() + { + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    cnn.com

    ', + $this->parsedown->text('[cnn.com](http://www.cnn.com)')); + $this->assertSame('

    google.com

    ', + $this->parsedown->text('[google.com](https://www.google.com)')); + } + + public function testExternalLinksSubDirAbsoluteUrls() + { + $this->config->set('system.absolute_urls', true); + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    cnn.com

    ', + $this->parsedown->text('[cnn.com](http://www.cnn.com)')); + $this->assertSame('

    google.com

    ', + $this->parsedown->text('[google.com](https://www.google.com)')); + } + + public function testAnchorLinksRelativeUrls() + { + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $this->assertSame('

    Current Anchor

    ', + $this->parsedown->text('[Current Anchor](#foo)')); + $this->assertSame('

    Root Anchor

    ', + $this->parsedown->text('[Root Anchor](/#foo)')); + $this->assertSame('

    Peer Anchor

    ', + $this->parsedown->text('[Peer Anchor](../item2-1#foo)')); + $this->assertSame('

    Peer Anchor 2

    ', + $this->parsedown->text('[Peer Anchor 2](../item2-1/#foo)')); + } + + public function testAnchorLinksAbsoluteUrls() + { + $this->config->set('system.absolute_urls', true); + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $this->assertSame('

    Current Anchor

    ', + $this->parsedown->text('[Current Anchor](#foo)')); + $this->assertSame('

    Peer Anchor

    ', + $this->parsedown->text('[Peer Anchor](../item2-1#foo)')); + $this->assertSame('

    Peer Anchor 2

    ', + $this->parsedown->text('[Peer Anchor 2](../item2-1/#foo)')); + $this->assertSame('

    Root Anchor

    ', + $this->parsedown->text('[Root Anchor](/#foo)')); + } + + public function testAnchorLinksWithPortAbsoluteUrls() + { + $this->config->set('system.absolute_urls', true); + $this->uri->initializeWithURL('http://testing.dev:8080/item2/item2-2')->init(); + + $this->assertSame('

    Peer Anchor

    ', + $this->parsedown->text('[Peer Anchor](../item2-1#foo)')); + $this->assertSame('

    Peer Anchor 2

    ', + $this->parsedown->text('[Peer Anchor 2](../item2-1/#foo)')); + $this->assertSame('

    Current Anchor

    ', + $this->parsedown->text('[Current Anchor](#foo)')); + $this->assertSame('

    Root Anchor

    ', + $this->parsedown->text('[Root Anchor](/#foo)')); + } + + public function testAnchorLinksSubDirRelativeUrls() + { + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    Peer Anchor

    ', + $this->parsedown->text('[Peer Anchor](../item2-1#foo)')); + $this->assertSame('

    Peer Anchor 2

    ', + $this->parsedown->text('[Peer Anchor 2](../item2-1/#foo)')); + $this->assertSame('

    Current Anchor

    ', + $this->parsedown->text('[Current Anchor](#foo)')); + $this->assertSame('

    Root Anchor

    ', + $this->parsedown->text('[Root Anchor](/#foo)')); + } + + public function testAnchorLinksSubDirAbsoluteUrls() + { + $this->config->set('system.absolute_urls', true); + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    Peer Anchor

    ', + $this->parsedown->text('[Peer Anchor](../item2-1#foo)')); + $this->assertSame('

    Peer Anchor 2

    ', + $this->parsedown->text('[Peer Anchor 2](../item2-1/#foo)')); + $this->assertSame('

    Current Anchor

    ', + $this->parsedown->text('[Current Anchor](#foo)')); + $this->assertSame('

    Root Anchor

    ', + $this->parsedown->text('[Root Anchor](/#foo)')); + } + + public function testSlugRelativeLinks() + { + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $this->assertSame('

    Up to Root Level

    ', + $this->parsedown->text('[Up to Root Level](../..)')); + $this->assertSame('

    Peer Page

    ', + $this->parsedown->text('[Peer Page](../item2-1)')); + $this->assertSame('

    Down a Level

    ', + $this->parsedown->text('[Down a Level](item2-2-1)')); + $this->assertSame('

    Up a Level

    ', + $this->parsedown->text('[Up a Level](..)')); + $this->assertSame('

    Up and Down

    ', + $this->parsedown->text('[Up and Down](../../item3/item3-3)')); + $this->assertSame('

    Down a Level with Query

    ', + $this->parsedown->text('[Down a Level with Query](item2-2-1?foo=bar)')); + $this->assertSame('

    Up a Level with Query

    ', + $this->parsedown->text('[Up a Level with Query](../?foo=bar)')); + $this->assertSame('

    Up and Down with Query

    ', + $this->parsedown->text('[Up and Down with Query](../../item3/item3-3?foo=bar)')); + $this->assertSame('

    Up and Down with Param

    ', + $this->parsedown->text('[Up and Down with Param](../../item3/item3-3/foo:bar)')); + $this->assertSame('

    Up and Down with Anchor

    ', + $this->parsedown->text('[Up and Down with Anchor](../../item3/item3-3#foo)')); + } + + public function testSlugRelativeLinksAbsoluteUrls() + { + $this->config->set('system.absolute_urls', true); + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $this->assertSame('

    Peer Page

    ', + $this->parsedown->text('[Peer Page](../item2-1)')); + $this->assertSame('

    Down a Level

    ', + $this->parsedown->text('[Down a Level](item2-2-1)')); + $this->assertSame('

    Up a Level

    ', + $this->parsedown->text('[Up a Level](..)')); + $this->assertSame('

    Up to Root Level

    ', + $this->parsedown->text('[Up to Root Level](../..)')); + $this->assertSame('

    Up and Down

    ', + $this->parsedown->text('[Up and Down](../../item3/item3-3)')); + $this->assertSame('

    Down a Level with Query

    ', + $this->parsedown->text('[Down a Level with Query](item2-2-1?foo=bar)')); + $this->assertSame('

    Up a Level with Query

    ', + $this->parsedown->text('[Up a Level with Query](../?foo=bar)')); + $this->assertSame('

    Up and Down with Query

    ', + $this->parsedown->text('[Up and Down with Query](../../item3/item3-3?foo=bar)')); + $this->assertSame('

    Up and Down with Param

    ', + $this->parsedown->text('[Up and Down with Param](../../item3/item3-3/foo:bar)')); + $this->assertSame('

    Up and Down with Anchor

    ', + $this->parsedown->text('[Up and Down with Anchor](../../item3/item3-3#foo)')); + } + + public function testSlugRelativeLinksSubDir() + { + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    Peer Page

    ', + $this->parsedown->text('[Peer Page](../item2-1)')); + $this->assertSame('

    Down a Level

    ', + $this->parsedown->text('[Down a Level](item2-2-1)')); + $this->assertSame('

    Up a Level

    ', + $this->parsedown->text('[Up a Level](..)')); + $this->assertSame('

    Up to Root Level

    ', + $this->parsedown->text('[Up to Root Level](../..)')); + $this->assertSame('

    Up and Down

    ', + $this->parsedown->text('[Up and Down](../../item3/item3-3)')); + $this->assertSame('

    Down a Level with Query

    ', + $this->parsedown->text('[Down a Level with Query](item2-2-1?foo=bar)')); + $this->assertSame('

    Up a Level with Query

    ', + $this->parsedown->text('[Up a Level with Query](../?foo=bar)')); + $this->assertSame('

    Up and Down with Query

    ', + $this->parsedown->text('[Up and Down with Query](../../item3/item3-3?foo=bar)')); + $this->assertSame('

    Up and Down with Param

    ', + $this->parsedown->text('[Up and Down with Param](../../item3/item3-3/foo:bar)')); + $this->assertSame('

    Up and Down with Anchor

    ', + $this->parsedown->text('[Up and Down with Anchor](../../item3/item3-3#foo)')); + } + + public function testSlugRelativeLinksSubDirAbsoluteUrls() + { + $this->config->set('system.absolute_urls', true); + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    Peer Page

    ', + $this->parsedown->text('[Peer Page](../item2-1)')); + $this->assertSame('

    Down a Level

    ', + $this->parsedown->text('[Down a Level](item2-2-1)')); + $this->assertSame('

    Up a Level

    ', + $this->parsedown->text('[Up a Level](..)')); + $this->assertSame('

    Up to Root Level

    ', + $this->parsedown->text('[Up to Root Level](../..)')); + $this->assertSame('

    Up and Down

    ', + $this->parsedown->text('[Up and Down](../../item3/item3-3)')); + $this->assertSame('

    Down a Level with Query

    ', + $this->parsedown->text('[Down a Level with Query](item2-2-1?foo=bar)')); + $this->assertSame('

    Up a Level with Query

    ', + $this->parsedown->text('[Up a Level with Query](../?foo=bar)')); + $this->assertSame('

    Up and Down with Query

    ', + $this->parsedown->text('[Up and Down with Query](../../item3/item3-3?foo=bar)')); + $this->assertSame('

    Up and Down with Param

    ', + $this->parsedown->text('[Up and Down with Param](../../item3/item3-3/foo:bar)')); + $this->assertSame('

    Up and Down with Anchor

    ', + $this->parsedown->text('[Up and Down with Anchor](../../item3/item3-3#foo)')); + } + + + public function testDirectoryRelativeLinks() + { + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $this->assertSame('

    Up and Down with Param

    ', + $this->parsedown->text('[Up and Down with Param](../../03.item3/03.item3-3/foo:bar)')); + $this->assertSame('

    Peer Page

    ', + $this->parsedown->text('[Peer Page](../01.item2-1)')); + $this->assertSame('

    Down a Level

    ', + $this->parsedown->text('[Down a Level](01.item2-2-1)')); + $this->assertSame('

    Up and Down

    ', + $this->parsedown->text('[Up and Down](../../03.item3/03.item3-3)')); + $this->assertSame('

    Down a Level with Query

    ', + $this->parsedown->text('[Down a Level with Query](01.item2-2-1?foo=bar)')); + $this->assertSame('

    Up and Down with Query

    ', + $this->parsedown->text('[Up and Down with Query](../../03.item3/03.item3-3?foo=bar)')); + $this->assertSame('

    Up and Down with Anchor

    ', + $this->parsedown->text('[Up and Down with Anchor](../../03.item3/03.item3-3#foo)')); + } + + + public function testAbsoluteLinks() + { + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $this->assertSame('

    Root

    ', + $this->parsedown->text('[Root](/)')); + $this->assertSame('

    Peer Page

    ', + $this->parsedown->text('[Peer Page](/item2/item2-1)')); + $this->assertSame('

    Down a Level

    ', + $this->parsedown->text('[Down a Level](/item2/item2-2/item2-2-1)')); + $this->assertSame('

    Up a Level

    ', + $this->parsedown->text('[Up a Level](/item2)')); + $this->assertSame('

    With Query

    ', + $this->parsedown->text('[With Query](/item2?foo=bar)')); + $this->assertSame('

    With Param

    ', + $this->parsedown->text('[With Param](/item2/foo:bar)')); + $this->assertSame('

    With Anchor

    ', + $this->parsedown->text('[With Anchor](/item2#foo)')); + } + + public function testDirectoryAbsoluteLinksSubDir() + { + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    Root

    ', + $this->parsedown->text('[Root](/)')); + $this->assertSame('

    Peer Page

    ', + $this->parsedown->text('[Peer Page](/item2/item2-1)')); + $this->assertSame('

    Down a Level

    ', + $this->parsedown->text('[Down a Level](/item2/item2-2/item2-2-1)')); + $this->assertSame('

    Up a Level

    ', + $this->parsedown->text('[Up a Level](/item2)')); + $this->assertSame('

    With Query

    ', + $this->parsedown->text('[With Query](/item2?foo=bar)')); + $this->assertSame('

    With Param

    ', + $this->parsedown->text('[With Param](/item2/foo:bar)')); + $this->assertSame('

    With Anchor

    ', + $this->parsedown->text('[With Anchor](/item2#foo)')); + } + + public function testDirectoryAbsoluteLinksSubDirAbsoluteUrl() + { + $this->config->set('system.absolute_urls', true); + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    Root

    ', + $this->parsedown->text('[Root](/)')); + $this->assertSame('

    Peer Page

    ', + $this->parsedown->text('[Peer Page](/item2/item2-1)')); + $this->assertSame('

    Down a Level

    ', + $this->parsedown->text('[Down a Level](/item2/item2-2/item2-2-1)')); + $this->assertSame('

    Up a Level

    ', + $this->parsedown->text('[Up a Level](/item2)')); + $this->assertSame('

    With Query

    ', + $this->parsedown->text('[With Query](/item2?foo=bar)')); + $this->assertSame('

    With Param

    ', + $this->parsedown->text('[With Param](/item2/foo:bar)')); + $this->assertSame('

    With Anchor

    ', + $this->parsedown->text('[With Anchor](/item2#foo)')); + } + + public function testSpecialProtocols() + { + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $this->assertSame('

    mailto

    ', + $this->parsedown->text('[mailto](mailto:user@domain.com)')); + $this->assertSame('

    xmpp

    ', + $this->parsedown->text('[xmpp](xmpp:xyx@domain.com)')); + $this->assertSame('

    tel

    ', + $this->parsedown->text('[tel](tel:123-555-12345)')); + $this->assertSame('

    sms

    ', + $this->parsedown->text('[sms](sms:123-555-12345)')); + $this->assertSame('

    ts.example.com

    ', + $this->parsedown->text('[ts.example.com](rdp://ts.example.com)')); + } + + public function testSpecialProtocolsSubDir() + { + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    mailto

    ', + $this->parsedown->text('[mailto](mailto:user@domain.com)')); + $this->assertSame('

    xmpp

    ', + $this->parsedown->text('[xmpp](xmpp:xyx@domain.com)')); + $this->assertSame('

    tel

    ', + $this->parsedown->text('[tel](tel:123-555-12345)')); + $this->assertSame('

    sms

    ', + $this->parsedown->text('[sms](sms:123-555-12345)')); + $this->assertSame('

    ts.example.com

    ', + $this->parsedown->text('[ts.example.com](rdp://ts.example.com)')); + } + + public function testSpecialProtocolsSubDirAbsoluteUrl() + { + $this->config->set('system.absolute_urls', true); + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    mailto

    ', + $this->parsedown->text('[mailto](mailto:user@domain.com)')); + $this->assertSame('

    xmpp

    ', + $this->parsedown->text('[xmpp](xmpp:xyx@domain.com)')); + $this->assertSame('

    tel

    ', + $this->parsedown->text('[tel](tel:123-555-12345)')); + $this->assertSame('

    sms

    ', + $this->parsedown->text('[sms](sms:123-555-12345)')); + $this->assertSame('

    ts.example.com

    ', + $this->parsedown->text('[ts.example.com](rdp://ts.example.com)')); + } + + public function testReferenceLinks() + { + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $sample = '[relative link][r_relative] + [r_relative]: ../item2-3#blah'; + $this->assertSame('

    relative link

    ', + $this->parsedown->text($sample)); + + $sample = '[absolute link][r_absolute] + [r_absolute]: /item3#blah'; + $this->assertSame('

    absolute link

    ', + $this->parsedown->text($sample)); + + $sample = '[external link][r_external] + [r_external]: http://www.cnn.com'; + $this->assertSame('

    external link

    ', + $this->parsedown->text($sample)); + } + + public function testAttributeLinks() + { + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $this->assertSame('

    Anchor Class

    ', + $this->parsedown->text('[Anchor Class](?classes=button#something)')); + $this->assertSame('

    Relative Class

    ', + $this->parsedown->text('[Relative Class](../item2-3?classes=button)')); + $this->assertSame('

    Relative ID

    ', + $this->parsedown->text('[Relative ID](../item2-3?id=unique)')); + $this->assertSame('

    External

    ', + $this->parsedown->text('[External](https://github.com/getgrav/grav?classes=button,big)')); + $this->assertSame('

    Relative Noprocess

    ', + $this->parsedown->text('[Relative Noprocess](../item2-3?id=unique&noprocess)')); + $this->assertSame('

    Relative Target

    ', + $this->parsedown->text('[Relative Target](../item2-3?target=_blank)')); + $this->assertSame('

    Relative Rel

    ', + $this->parsedown->text('[Relative Rel](../item2-3?rel=nofollow)')); + $this->assertSame('

    Relative Mixed

    ', + $this->parsedown->text('[Relative Mixed](../item2-3?foo=bar&baz=qux&rel=nofollow&class=button)')); + } + + public function testInvalidLinks() + { + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + + $this->assertSame('

    Non Existent Page

    ', + $this->parsedown->text('[Non Existent Page](no-page)')); + $this->assertSame('

    Existent File

    ', + $this->parsedown->text('[Existent File](existing-file.zip)')); + $this->assertSame('

    Non Existent File

    ', + $this->parsedown->text('[Non Existent File](missing-file.zip)')); + } + + public function testInvalidLinksSubDir() + { + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    Non Existent Page

    ', + $this->parsedown->text('[Non Existent Page](no-page)')); + $this->assertSame('

    Existent File

    ', + $this->parsedown->text('[Existent File](existing-file.zip)')); + $this->assertSame('

    Non Existent File

    ', + $this->parsedown->text('[Non Existent File](missing-file.zip)')); + } + + public function testInvalidLinksSubDirAbsoluteUrl() + { + $this->config->set('system.absolute_urls', true); + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/item2/item2-2', '/subdir')->init(); + + $this->assertSame('

    Non Existent Page

    ', + $this->parsedown->text('[Non Existent Page](no-page)')); + $this->assertSame('

    Existent File

    ', + $this->parsedown->text('[Existent File](existing-file.zip)')); + $this->assertSame('

    Non Existent File

    ', + $this->parsedown->text('[Non Existent File](missing-file.zip)')); + } + + + /** + * @param $string + * + * @return mixed + */ + private function stripLeadingWhitespace($string) + { + return preg_replace('/^\s*(.*)/', '', $string); + } + +} diff --git a/tests/unit/Grav/Common/Page/PagesTest.php b/tests/unit/Grav/Common/Page/PagesTest.php new file mode 100644 index 0000000..831ee3d --- /dev/null +++ b/tests/unit/Grav/Common/Page/PagesTest.php @@ -0,0 +1,281 @@ +grav = $grav(); + $this->pages = $this->grav['pages']; + $this->grav['config']->set('system.home.alias', '/home'); + + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + $locator->addPath('page', '', 'tests/fake/simple-site/user/pages', false); + $this->pages->init(); + } + + public function testBase() + { + $this->assertSame('', $this->pages->base()); + $this->pages->base('/test'); + $this->assertSame('/test', $this->pages->base()); + $this->pages->base(''); + $this->assertNull($this->pages->base()); + } + + public function testLastModified() + { + $this->assertNull($this->pages->lastModified()); + $this->pages->lastModified('test'); + $this->assertSame('test', $this->pages->lastModified()); + } + + public function testInstances() + { + $this->assertInternalType('array', $this->pages->instances()); + foreach($this->pages->instances() as $instance) { + $this->assertInstanceOf('Grav\Common\Page\Page', $instance); + } + } + + public function testRoutes() + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + $this->assertInternalType('array', $this->pages->routes()); + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/01.home', $this->pages->routes()['/']); + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/01.home', $this->pages->routes()['/home']); + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog', $this->pages->routes()['/blog']); + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one', $this->pages->routes()['/blog/post-one']); + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two', $this->pages->routes()['/blog/post-two']); + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/03.about', $this->pages->routes()['/about']); + } + + public function testAddPage() + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + $path = $locator->findResource('tests://') . '/fake/single-pages/01.simple-page/default.md'; + $aPage = new Page(); + $aPage->init(new \SplFileInfo($path)); + + $this->pages->addPage($aPage, '/new-page'); + + $this->assertContains('/new-page', array_keys($this->pages->routes())); + $this->assertSame($locator->findResource('tests://') . '/fake/single-pages/01.simple-page', $this->pages->routes()['/new-page']); + } + + public function testSort() + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + $aPage = $this->pages->dispatch('/blog'); + $subPagesSorted = $this->pages->sort($aPage); + + $this->assertInternalType('array', $subPagesSorted); + $this->assertCount(2, $subPagesSorted); + + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one', array_keys($subPagesSorted)[0]); + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two', array_keys($subPagesSorted)[1]); + + $this->assertContains($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one', array_keys($subPagesSorted)); + $this->assertContains($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two', array_keys($subPagesSorted)); + + $this->assertSame(["slug" => "post-one"], $subPagesSorted[$locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one']); + $this->assertSame(["slug" => "post-two"], $subPagesSorted[$locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two']); + + $subPagesSorted = $this->pages->sort($aPage, null, 'desc'); + + $this->assertInternalType('array', $subPagesSorted); + $this->assertCount(2, $subPagesSorted); + + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two', array_keys($subPagesSorted)[0]); + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one', array_keys($subPagesSorted)[1]); + + $this->assertContains($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one', array_keys($subPagesSorted)); + $this->assertContains($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two', array_keys($subPagesSorted)); + + $this->assertSame(["slug" => "post-one"], $subPagesSorted[$locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one']); + $this->assertSame(["slug" => "post-two"], $subPagesSorted[$locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two']); + } + + public function testSortCollection() + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + $aPage = $this->pages->dispatch('/blog'); + $subPagesSorted = $this->pages->sortCollection($aPage->children(), $aPage->orderBy()); + + $this->assertInternalType('array', $subPagesSorted); + $this->assertCount(2, $subPagesSorted); + + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one', array_keys($subPagesSorted)[0]); + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two', array_keys($subPagesSorted)[1]); + + $this->assertContains($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one', array_keys($subPagesSorted)); + $this->assertContains($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two', array_keys($subPagesSorted)); + + $this->assertSame(["slug" => "post-one"], $subPagesSorted[$locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one']); + $this->assertSame(["slug" => "post-two"], $subPagesSorted[$locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two']); + + $subPagesSorted = $this->pages->sortCollection($aPage->children(), $aPage->orderBy(), 'desc'); + + $this->assertInternalType('array', $subPagesSorted); + $this->assertCount(2, $subPagesSorted); + + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two', array_keys($subPagesSorted)[0]); + $this->assertSame($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one', array_keys($subPagesSorted)[1]); + + $this->assertContains($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one', array_keys($subPagesSorted)); + $this->assertContains($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two', array_keys($subPagesSorted)); + + $this->assertSame(["slug" => "post-one"], $subPagesSorted[$locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-one']); + $this->assertSame(["slug" => "post-two"], $subPagesSorted[$locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog/post-two']); + } + + public function testGet() + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + //Page existing + $aPage = $this->pages->get($locator->findResource('tests://') . '/fake/simple-site/user/pages/03.about'); + $this->assertInternalType('object', $aPage); + $this->assertInstanceOf('Grav\Common\Page\Page', $aPage); + + //Page not existing + $anotherPage = $this->pages->get($locator->findResource('tests://') . '/fake/simple-site/user/pages/03.non-existing'); + $this->assertNotInternalType('object', $anotherPage); + $this->assertNull($anotherPage); + } + + public function testChildren() + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + //Page existing + $children = $this->pages->children($locator->findResource('tests://') . '/fake/simple-site/user/pages/02.blog'); + $this->assertInstanceOf('Grav\Common\Page\Collection', $children); + + //Page not existing + $children = $this->pages->children($locator->findResource('tests://') . '/fake/whatever/non-existing'); + $this->assertSame([], $children->toArray()); + } + + public function testDispatch() + { + $aPage = $this->pages->dispatch('/blog'); + $this->assertInstanceOf('Grav\Common\Page\Page', $aPage); + + $aPage = $this->pages->dispatch('/about'); + $this->assertInstanceOf('Grav\Common\Page\Page', $aPage); + + $aPage = $this->pages->dispatch('/blog/post-one'); + $this->assertInstanceOf('Grav\Common\Page\Page', $aPage); + + //Page not existing + $aPage = $this->pages->dispatch('/non-existing'); + $this->assertNull($aPage); + } + + public function testRoot() + { + $root = $this->pages->root(); + $this->assertInstanceOf('Grav\Common\Page\Page', $root); + $this->assertSame('pages', $root->folder()); + } + + public function testBlueprints() + { + + } + + public function testAll() + { + $this->assertInternalType('object', $this->pages->all()); + $this->assertInternalType('array', $this->pages->all()->toArray()); + foreach($this->pages->all() as $page) { + $this->assertInstanceOf('Grav\Common\Page\Page', $page); + } + } + + public function testGetList() + { + $list = $this->pages->getList(); + $this->assertInternalType('array', $list); + $this->assertSame('—-▸ Home', $list['/']); + $this->assertSame('—-▸ Blog', $list['/blog']); + } + + public function testGetTypes() + { + + } + + public function testTypes() + { + + } + + public function testModularTypes() + { + + } + + public function testPageTypes() + { + + } + + public function testAccessLevels() + { + + } + + public function testParents() + { + + } + + public function testParentsRawRoutes() + { + + } + + public function testGetHomeRoute() + { + + } + + public function testResetPages() + { + + } + +} diff --git a/tests/unit/Grav/Common/Twig/TwigExtensionTest.php b/tests/unit/Grav/Common/Twig/TwigExtensionTest.php new file mode 100644 index 0000000..56181f9 --- /dev/null +++ b/tests/unit/Grav/Common/Twig/TwigExtensionTest.php @@ -0,0 +1,208 @@ +grav = Fixtures::get('grav'); + $this->twig_ext = new TwigExtension(); + } + + public function testInflectorFilter() + { + $this->assertSame('people', $this->twig_ext->inflectorFilter('plural', 'person')); + $this->assertSame('shoe', $this->twig_ext->inflectorFilter('singular', 'shoes')); + $this->assertSame('Welcome Page', $this->twig_ext->inflectorFilter('title', 'welcome page')); + $this->assertSame('SendEmail', $this->twig_ext->inflectorFilter('camel', 'send_email')); + $this->assertSame('camel_cased', $this->twig_ext->inflectorFilter('underscor', 'CamelCased')); + $this->assertSame('something-text', $this->twig_ext->inflectorFilter('hyphen', 'Something Text')); + $this->assertSame('Something text to read', $this->twig_ext->inflectorFilter('human', 'something_text_to_read')); + $this->assertSame(5, $this->twig_ext->inflectorFilter('month', 175)); + $this->assertSame('10th', $this->twig_ext->inflectorFilter('ordinal', 10)); + } + + public function testMd5Filter() + { + $this->assertSame(md5('grav'), $this->twig_ext->md5Filter('grav')); + $this->assertSame(md5('devs@getgrav.org'), $this->twig_ext->md5Filter('devs@getgrav.org')); + } + + public function testKsortFilter() + { + $object = array("name"=>"Bob","age"=>8,"colour"=>"red"); + $this->assertSame(array("age"=>8,"colour"=>"red","name"=>"Bob"), $this->twig_ext->ksortFilter($object)); + } + + public function testContainsFilter() + { + $this->assertTrue($this->twig_ext->containsFilter('grav','grav')); + $this->assertTrue($this->twig_ext->containsFilter('So, I found this new cms, called grav, and it\'s pretty awesome guys','grav')); + } + + public function testNicetimeFilter() + { + $now = time(); + $threeMinutes = time() - (60*3); + $threeHours = time() - (60*60*3); + $threeDays = time() - (60*60*24*3); + $threeMonths = time() - (60*60*24*30*3); + $threeYears = time() - (60*60*24*365*3); + $measures = ['minutes','hours','days','months','years']; + + $this->assertSame('No date provided', $this->twig_ext->nicetimeFunc(null)); + + for ($i=0; $iassertSame('3 ' . $measures[$i] . ' ago', $this->twig_ext->nicetimeFunc($$time)); + } + } + + public function testRandomizeFilter() + { + $array = [1,2,3,4,5]; + $this->assertContains(2, $this->twig_ext->randomizeFilter($array)); + $this->assertSame($array, $this->twig_ext->randomizeFilter($array, 5)); + $this->assertSame($array[0], $this->twig_ext->randomizeFilter($array, 1)[0]); + $this->assertSame($array[3], $this->twig_ext->randomizeFilter($array, 4)[3]); + $this->assertSame($array[1], $this->twig_ext->randomizeFilter($array, 4)[1]); + } + + public function testModulusFilter() + { + $this->assertSame(3, $this->twig_ext->modulusFilter(3,4)); + $this->assertSame(1, $this->twig_ext->modulusFilter(11,2)); + $this->assertSame(0, $this->twig_ext->modulusFilter(10,2)); + $this->assertSame(2, $this->twig_ext->modulusFilter(10,4)); + } + + public function testAbsoluteUrlFilter() + { + + } + + public function testMarkdownFilter() + { + + } + + public function testStartsWithFilter() + { + + } + + public function testEndsWithFilter() + { + + } + + public function testDefinedDefaultFilter() + { + + } + + public function testRtrimFilter() + { + + } + + public function testLtrimFilter() + { + + } + + public function testRepeatFunc() + { + + } + + public function testRegexReplace() + { + + } + + public function testUrlFunc() + { + + } + + public function testEvaluateFunc() + { + + } + + public function testDump() + { + + } + + public function testGistFunc() + { + + } + + public function testRandomStringFunc() + { + + } + + public function testPadFilter() + { + + } + + public function testArrayFunc() + { + $this->assertSame('this is my text', + $this->twig_ext->regexReplace('

    this is my text

    ', '(<\/?p>)', '')); + $this->assertSame('this is my text', + $this->twig_ext->regexReplace('

    this is my text

    ', ['(

    )','(<\/p>)'], ['',''])); + } + + public function testArrayKeyValue() + { + $this->assertSame(['meat' => 'steak'], + $this->twig_ext->arrayKeyValueFunc('meat', 'steak')); + $this->assertSame(['fruit' => 'apple', 'meat' => 'steak'], + $this->twig_ext->arrayKeyValueFunc('meat', 'steak', ['fruit' => 'apple'])); + } + + public function stringFunc() + { + + } + + public function testRangeFunc() + { + $hundred = []; + for($i = 0; $i <= 100; $i++) { $hundred[] = $i; } + + + $this->assertSame([0], $this->twig_ext->rangeFunc(0, 0)); + $this->assertSame([0, 1, 2], $this->twig_ext->rangeFunc(0, 2)); + + $this->assertSame([0, 5, 10, 15], $this->twig_ext->rangeFunc(0, 16, 5)); + + // default (min 0, max 100, step 1) + $this->assertSame($hundred, $this->twig_ext->rangeFunc()); + + // 95 items, starting from 5, (min 5, max 100, step 1) + $this->assertSame(array_slice($hundred, 5), $this->twig_ext->rangeFunc(5)); + + // reversed range + $this->assertSame(array_reverse($hundred), $this->twig_ext->rangeFunc(100, 0)); + $this->assertSame([4, 2, 0], $this->twig_ext->rangeFunc(4, 0, 2)); + } +} diff --git a/tests/unit/Grav/Common/UriTest.php b/tests/unit/Grav/Common/UriTest.php new file mode 100644 index 0000000..9977d67 --- /dev/null +++ b/tests/unit/Grav/Common/UriTest.php @@ -0,0 +1,1153 @@ + [ + 'scheme' => '', + 'user' => null, + 'password' => null, + 'host' => null, + 'port' => null, + 'path' => '/path', + 'query' => '', + 'fragment' => null, + + 'route' => '/path', + 'paths' => ['path'], + 'params' => null, + 'url' => '/path', + 'environment' => 'unknown', + 'basename' => 'path', + 'base' => '', + 'currentPage' => 1, + 'rootUrl' => '', + 'extension' => null, + 'addNonce' => '/path/nonce:{{nonce}}', + ], + '//localhost/' => [ + 'scheme' => '//', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => null, + 'path' => '/', + 'query' => '', + 'fragment' => null, + + 'route' => '/', + 'paths' => [], + 'params' => null, + 'url' => '/', + 'environment' => 'localhost', + 'basename' => '', + 'base' => '//localhost', + 'currentPage' => 1, + 'rootUrl' => '//localhost', + 'extension' => null, + 'addNonce' => '//localhost/nonce:{{nonce}}', + ], + 'http://localhost/' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/', + 'query' => '', + 'fragment' => null, + + 'route' => '/', + 'paths' => [], + 'params' => null, + 'url' => '/', + 'environment' => 'localhost', + 'basename' => '', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => null, + 'addNonce' => 'http://localhost/nonce:{{nonce}}', + ], + 'http://127.0.0.1/' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => '127.0.0.1', + 'port' => 80, + 'path' => '/', + 'query' => '', + 'fragment' => null, + + 'route' => '/', + 'paths' => [], + 'params' => null, + 'url' => '/', + 'environment' => 'localhost', + 'basename' => '', + 'base' => 'http://127.0.0.1', + 'currentPage' => 1, + 'rootUrl' => 'http://127.0.0.1', + 'extension' => null, + 'addNonce' => 'http://127.0.0.1/nonce:{{nonce}}', + ], + 'https://localhost/' => [ + 'scheme' => 'https://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 443, + 'path' => '/', + 'query' => '', + 'fragment' => null, + + 'route' => '/', + 'paths' => [], + 'params' => null, + 'url' => '/', + 'environment' => 'localhost', + 'basename' => '', + 'base' => 'https://localhost', + 'currentPage' => 1, + 'rootUrl' => 'https://localhost', + 'extension' => null, + 'addNonce' => 'https://localhost/nonce:{{nonce}}', + ], + 'http://localhost:8080/grav/it/ueper' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 8080, + 'path' => '/grav/it/ueper', + 'query' => '', + 'fragment' => null, + + 'route' => '/grav/it/ueper', + 'paths' => ['grav', 'it', 'ueper'], + 'params' => null, + 'url' => '/grav/it/ueper', + 'environment' => 'localhost', + 'basename' => 'ueper', + 'base' => 'http://localhost:8080', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost:8080', + 'extension' => null, + 'addNonce' => 'http://localhost:8080/grav/it/ueper/nonce:{{nonce}}', + ], + 'http://localhost:8080/grav/it/ueper:xxx' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 8080, + 'path' => '/grav/it', + 'query' => '', + 'fragment' => null, + + 'route' => '/grav/it', + 'paths' => ['grav', 'it'], + 'params' => '/ueper:xxx', + 'url' => '/grav/it', + 'environment' => 'localhost', + 'basename' => 'it', + 'base' => 'http://localhost:8080', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost:8080', + 'extension' => null, + 'addNonce' => 'http://localhost:8080/grav/it/ueper:xxx/nonce:{{nonce}}', + ], + 'http://localhost:8080/grav/it/ueper:xxx/page:/test:yyy' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 8080, + 'path' => '/grav/it', + 'query' => '', + 'fragment' => null, + + 'route' => '/grav/it', + 'paths' => ['grav', 'it'], + 'params' => '/ueper:xxx/page:/test:yyy', + 'url' => '/grav/it', + 'environment' => 'localhost', + 'basename' => 'it', + 'base' => 'http://localhost:8080', + 'currentPage' => '', + 'rootUrl' => 'http://localhost:8080', + 'extension' => null, + 'addNonce' => 'http://localhost:8080/grav/it/ueper:xxx/page:/test:yyy/nonce:{{nonce}}', + ], + 'http://localhost:8080/grav/it/ueper?test=x' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 8080, + 'path' => '/grav/it/ueper', + 'query' => 'test=x', + 'fragment' => null, + + 'route' => '/grav/it/ueper', + 'paths' => ['grav', 'it', 'ueper'], + 'params' => null, + 'url' => '/grav/it/ueper', + 'environment' => 'localhost', + 'basename' => 'ueper', + 'base' => 'http://localhost:8080', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost:8080', + 'extension' => null, + 'addNonce' => 'http://localhost:8080/grav/it/ueper/nonce:{{nonce}}?test=x', + ], + 'http://localhost:80/grav/it/ueper?test=x' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/grav/it/ueper', + 'query' => 'test=x', + 'fragment' => null, + + 'route' => '/grav/it/ueper', + 'paths' => ['grav', 'it', 'ueper'], + 'params' => null, + 'url' => '/grav/it/ueper', + 'environment' => 'localhost', + 'basename' => 'ueper', + 'base' => 'http://localhost:80', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost:80', + 'extension' => null, + 'addNonce' => 'http://localhost:80/grav/it/ueper/nonce:{{nonce}}?test=x', + ], + 'http://localhost/grav/it/ueper?test=x' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/grav/it/ueper', + 'query' => 'test=x', + 'fragment' => null, + + 'route' => '/grav/it/ueper', + 'paths' => ['grav', 'it', 'ueper'], + 'params' => null, + 'url' => '/grav/it/ueper', + 'environment' => 'localhost', + 'basename' => 'ueper', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => null, + 'addNonce' => 'http://localhost/grav/it/ueper/nonce:{{nonce}}?test=x', + ], + 'http://grav/grav/it/ueper' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'grav', + 'port' => 80, + 'path' => '/grav/it/ueper', + 'query' => '', + 'fragment' => null, + + 'route' => '/grav/it/ueper', + 'paths' => ['grav', 'it', 'ueper'], + 'params' => null, + 'url' => '/grav/it/ueper', + 'environment' => 'grav', + 'basename' => 'ueper', + 'base' => 'http://grav', + 'currentPage' => 1, + 'rootUrl' => 'http://grav', + 'extension' => null, + 'addNonce' => 'http://grav/grav/it/ueper/nonce:{{nonce}}', + ], + 'https://username:password@api.getgrav.com:4040/v1/post/128/page:x/?all=1' => [ + 'scheme' => 'https://', + 'user' => 'username', + 'password' => 'password', + 'host' => 'api.getgrav.com', + 'port' => 4040, + 'path' => '/v1/post/128/', // FIXME <- + 'query' => 'all=1', + 'fragment' => null, + + 'route' => '/v1/post/128', + 'paths' => ['v1', 'post', '128'], + 'params' => '/page:x', + 'url' => '/v1/post/128', + 'environment' => 'api.getgrav.com', + 'basename' => '128', + 'base' => 'https://api.getgrav.com:4040', + 'currentPage' => 'x', + 'rootUrl' => 'https://api.getgrav.com:4040', + 'extension' => null, + 'addNonce' => 'https://username:password@api.getgrav.com:4040/v1/post/128/page:x/nonce:{{nonce}}?all=1', + 'toOriginalString' => 'https://username:password@api.getgrav.com:4040/v1/post/128/page:x?all=1' + ], + 'https://google.com:443/' => [ + 'scheme' => 'https://', + 'user' => null, + 'password' => null, + 'host' => 'google.com', + 'port' => 443, + 'path' => '/', + 'query' => '', + 'fragment' => null, + + 'route' => '/', + 'paths' => [], + 'params' => null, + 'url' => '/', + 'environment' => 'google.com', + 'basename' => '', + 'base' => 'https://google.com:443', + 'currentPage' => 1, + 'rootUrl' => 'https://google.com:443', + 'extension' => null, + 'addNonce' => 'https://google.com:443/nonce:{{nonce}}', + ], + // Path tests. + 'http://localhost:8080/a/b/c/d' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 8080, + 'path' => '/a/b/c/d', + 'query' => '', + 'fragment' => null, + + 'route' => '/a/b/c/d', + 'paths' => ['a', 'b', 'c', 'd'], + 'params' => null, + 'url' => '/a/b/c/d', + 'environment' => 'localhost', + 'basename' => 'd', + 'base' => 'http://localhost:8080', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost:8080', + 'extension' => null, + 'addNonce' => 'http://localhost:8080/a/b/c/d/nonce:{{nonce}}', + ], + 'http://localhost:8080/a/b/c/d/e/f/a/b/c/d/e/f/a/b/c/d/e/f' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 8080, + 'path' => '/a/b/c/d/e/f/a/b/c/d/e/f/a/b/c/d/e/f', + 'query' => '', + 'fragment' => null, + + 'route' => '/a/b/c/d/e/f/a/b/c/d/e/f/a/b/c/d/e/f', + 'paths' => ['a', 'b', 'c', 'd', 'e', 'f', 'a', 'b', 'c', 'd', 'e', 'f', 'a', 'b', 'c', 'd', 'e', 'f'], + 'params' => null, + 'url' => '/a/b/c/d/e/f/a/b/c/d/e/f/a/b/c/d/e/f', + 'environment' => 'localhost', + 'basename' => 'f', + 'base' => 'http://localhost:8080', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost:8080', + 'extension' => null, + 'addNonce' => 'http://localhost:8080/a/b/c/d/e/f/a/b/c/d/e/f/a/b/c/d/e/f/nonce:{{nonce}}', + ], + 'http://localhost/this is the path/my page' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/this%20is%20the%20path/my%20page', + 'query' => '', + 'fragment' => null, + + 'route' => '/this%20is%20the%20path/my%20page', + 'paths' => ['this%20is%20the%20path', 'my%20page'], + 'params' => null, + 'url' => '/this%20is%20the%20path/my%20page', + 'environment' => 'localhost', + 'basename' => 'my%20page', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => null, + 'addNonce' => 'http://localhost/this%20is%20the%20path/my%20page/nonce:{{nonce}}', + 'toOriginalString' => 'http://localhost/this%20is%20the%20path/my%20page' + ], + 'http://localhost/pölöpölö/päläpälä' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/p%C3%B6l%C3%B6p%C3%B6l%C3%B6/p%C3%A4l%C3%A4p%C3%A4l%C3%A4', + 'query' => '', + 'fragment' => null, + + 'route' => '/p%C3%B6l%C3%B6p%C3%B6l%C3%B6/p%C3%A4l%C3%A4p%C3%A4l%C3%A4', + 'paths' => ['p%C3%B6l%C3%B6p%C3%B6l%C3%B6', 'p%C3%A4l%C3%A4p%C3%A4l%C3%A4'], + 'params' => null, + 'url' => '/p%C3%B6l%C3%B6p%C3%B6l%C3%B6/p%C3%A4l%C3%A4p%C3%A4l%C3%A4', + 'environment' => 'localhost', + 'basename' => 'p%C3%A4l%C3%A4p%C3%A4l%C3%A4', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => null, + 'addNonce' => 'http://localhost/p%C3%B6l%C3%B6p%C3%B6l%C3%B6/p%C3%A4l%C3%A4p%C3%A4l%C3%A4/nonce:{{nonce}}', + 'toOriginalString' => 'http://localhost/p%C3%B6l%C3%B6p%C3%B6l%C3%B6/p%C3%A4l%C3%A4p%C3%A4l%C3%A4' + ], + // Query params tests. + 'http://localhost:8080/grav/it/ueper?test=x&test2=y' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 8080, + 'path' => '/grav/it/ueper', + 'query' => 'test=x&test2=y', + 'fragment' => null, + + 'route' => '/grav/it/ueper', + 'paths' => ['grav', 'it', 'ueper'], + 'params' => null, + 'url' => '/grav/it/ueper', + 'environment' => 'localhost', + 'basename' => 'ueper', + 'base' => 'http://localhost:8080', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost:8080', + 'extension' => null, + 'addNonce' => 'http://localhost:8080/grav/it/ueper/nonce:{{nonce}}?test=x&test2=y', + ], + 'http://localhost:8080/grav/it/ueper?test=x&test2=y&test3=x&test4=y' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 8080, + 'path' => '/grav/it/ueper', + 'query' => 'test=x&test2=y&test3=x&test4=y', + 'fragment' => null, + + 'route' => '/grav/it/ueper', + 'paths' => ['grav', 'it', 'ueper'], + 'params' => null, + 'url' => '/grav/it/ueper', + 'environment' => 'localhost', + 'basename' => 'ueper', + 'base' => 'http://localhost:8080', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost:8080', + 'extension' => null, + 'addNonce' => 'http://localhost:8080/grav/it/ueper/nonce:{{nonce}}?test=x&test2=y&test3=x&test4=y', + ], + 'http://localhost:8080/grav/it/ueper?test=x&test2=y&test3=x&test4=y/test' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 8080, + 'path' => '/grav/it/ueper', + 'query' => 'test=x&test2=y&test3=x&test4=y%2Ftest', + 'fragment' => null, + + 'route' => '/grav/it/ueper', + 'paths' => ['grav', 'it', 'ueper'], + 'params' => null, + 'url' => '/grav/it/ueper', + 'environment' => 'localhost', + 'basename' => 'ueper', + 'base' => 'http://localhost:8080', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost:8080', + 'extension' => null, + 'addNonce' => 'http://localhost:8080/grav/it/ueper/nonce:{{nonce}}?test=x&test2=y&test3=x&test4=y/test', + ], + // Port tests. + 'http://localhost/a-page' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/a-page', + 'query' => '', + 'fragment' => null, + + 'route' => '/a-page', + 'paths' => ['a-page'], + 'params' => null, + 'url' => '/a-page', + 'environment' => 'localhost', + 'basename' => 'a-page', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => null, + 'addNonce' => 'http://localhost/a-page/nonce:{{nonce}}', + ], + 'http://localhost:8080/a-page' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 8080, + 'path' => '/a-page', + 'query' => '', + 'fragment' => null, + + 'route' => '/a-page', + 'paths' => ['a-page'], + 'params' => null, + 'url' => '/a-page', + 'environment' => 'localhost', + 'basename' => 'a-page', + 'base' => 'http://localhost:8080', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost:8080', + 'extension' => null, + 'addNonce' => 'http://localhost:8080/a-page/nonce:{{nonce}}', + ], + 'http://localhost:443/a-page' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 443, + 'path' => '/a-page', + 'query' => '', + 'fragment' => null, + + 'route' => '/a-page', + 'paths' => ['a-page'], + 'params' => null, + 'url' => '/a-page', + 'environment' => 'localhost', + 'basename' => 'a-page', + 'base' => 'http://localhost:443', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost:443', + 'extension' => null, + 'addNonce' => 'http://localhost:443/a-page/nonce:{{nonce}}', + ], + // Extension tests. + 'http://localhost/a-page.html' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/a-page', + 'query' => '', + 'fragment' => null, + + 'route' => '/a-page', + 'paths' => ['a-page'], + 'params' => null, + 'url' => '/a-page', + 'environment' => 'localhost', + 'basename' => 'a-page.html', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => 'html', + 'addNonce' => 'http://localhost/a-page.html/nonce:{{nonce}}', + 'toOriginalString' => 'http://localhost/a-page.html', + ], + 'http://localhost/a-page.json' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/a-page', + 'query' => '', + 'fragment' => null, + + 'route' => '/a-page', + 'paths' => ['a-page'], + 'params' => null, + 'url' => '/a-page', + 'environment' => 'localhost', + 'basename' => 'a-page.json', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => 'json', + 'addNonce' => 'http://localhost/a-page.json/nonce:{{nonce}}', + 'toOriginalString' => 'http://localhost/a-page.json', + ], + 'http://localhost/admin/ajax.json/task:getnewsfeed' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/admin/ajax', + 'query' => '', + 'fragment' => null, + + 'route' => '/admin/ajax', + 'paths' => ['admin', 'ajax'], + 'params' => '/task:getnewsfeed', + 'url' => '/admin/ajax', + 'environment' => 'localhost', + 'basename' => 'ajax.json', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => 'json', + 'addNonce' => 'http://localhost/admin/ajax.json/task:getnewsfeed/nonce:{{nonce}}', + 'toOriginalString' => 'http://localhost/admin/ajax.json/task:getnewsfeed', + ], + 'http://localhost/grav/admin/media.json/route:L1VzZXJzL3JodWsvd29ya3NwYWNlL2dyYXYtZGVtby1zYW1wbGVyL3VzZXIvYXNzZXRzL3FRMXB4Vk1ERTNJZzh5Ni5qcGc=/task:removeFileFromBlueprint/proute:/blueprint:Y29uZmlnL2RldGFpbHM=/type:config/field:deep.nested.custom_file/path:dXNlci9hc3NldHMvcVExcHhWTURFM0lnOHk2LmpwZw==' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/grav/admin/media', + 'query' => '', + 'fragment' => null, + + 'route' => '/grav/admin/media', + 'paths' => ['grav','admin','media'], + 'params' => '/route:L1VzZXJzL3JodWsvd29ya3NwYWNlL2dyYXYtZGVtby1zYW1wbGVyL3VzZXIvYXNzZXRzL3FRMXB4Vk1ERTNJZzh5Ni5qcGc=/task:removeFileFromBlueprint/proute:/blueprint:Y29uZmlnL2RldGFpbHM=/type:config/field:deep.nested.custom_file/path:dXNlci9hc3NldHMvcVExcHhWTURFM0lnOHk2LmpwZw==', + 'url' => '/grav/admin/media', + 'environment' => 'localhost', + 'basename' => 'media.json', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => 'json', + 'addNonce' => 'http://localhost/grav/admin/media.json/route:L1VzZXJzL3JodWsvd29ya3NwYWNlL2dyYXYtZGVtby1zYW1wbGVyL3VzZXIvYXNzZXRzL3FRMXB4Vk1ERTNJZzh5Ni5qcGc=/task:removeFileFromBlueprint/proute:/blueprint:Y29uZmlnL2RldGFpbHM=/type:config/field:deep.nested.custom_file/path:dXNlci9hc3NldHMvcVExcHhWTURFM0lnOHk2LmpwZw==/nonce:{{nonce}}', + 'toOriginalString' => 'http://localhost/grav/admin/media.json/route:L1VzZXJzL3JodWsvd29ya3NwYWNlL2dyYXYtZGVtby1zYW1wbGVyL3VzZXIvYXNzZXRzL3FRMXB4Vk1ERTNJZzh5Ni5qcGc=/task:removeFileFromBlueprint/proute:/blueprint:Y29uZmlnL2RldGFpbHM=/type:config/field:deep.nested.custom_file/path:dXNlci9hc3NldHMvcVExcHhWTURFM0lnOHk2LmpwZw==', + ], + 'http://localhost/a-page.foo' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/a-page.foo', + 'query' => '', + 'fragment' => null, + + 'route' => '/a-page.foo', + 'paths' => ['a-page.foo'], + 'params' => null, + 'url' => '/a-page.foo', + 'environment' => 'localhost', + 'basename' => 'a-page.foo', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => 'foo', + 'addNonce' => 'http://localhost/a-page.foo/nonce:{{nonce}}', + 'toOriginalString' => 'http://localhost/a-page.foo' + ], + // Fragment tests. + 'http://localhost:8080/a/b/c#my-fragment' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 8080, + 'path' => '/a/b/c', + 'query' => '', + 'fragment' => 'my-fragment', + + 'route' => '/a/b/c', + 'paths' => ['a', 'b', 'c'], + 'params' => null, + 'url' => '/a/b/c', + 'environment' => 'localhost', + 'basename' => 'c', + 'base' => 'http://localhost:8080', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost:8080', + 'extension' => null, + 'addNonce' => 'http://localhost:8080/a/b/c/nonce:{{nonce}}#my-fragment', + ], + // Attacks. + '">://localhost' => [ + 'scheme' => '', + 'user' => null, + 'password' => null, + 'host' => null, + 'port' => null, + 'path' => '%22%3E%3Cscript%3Ealert%3C/localhost', + 'query' => '', + 'fragment' => null, + + 'route' => '/%22%3E%3Cscript%3Ealert%3C/localhost', + 'paths' => ['%22%3E%3Cscript%3Ealert%3C', 'localhost'], + 'params' => '/script%3E:', + 'url' => '%22%3E%3Cscript%3Ealert%3C//localhost', + 'environment' => 'unknown', + 'basename' => 'localhost', + 'base' => '', + 'currentPage' => 1, + 'rootUrl' => '', + 'extension' => null, + //'addNonce' => '%22%3E%3Cscript%3Ealert%3C/localhost/script%3E:/nonce:{{nonce}}', // FIXME <- + 'toOriginalString' => '%22%3E%3Cscript%3Ealert%3C/localhost/script%3E:' // FIXME <- + ], + 'http://">' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'unknown', + 'port' => 80, + 'path' => '/script%3E', + 'query' => '', + 'fragment' => null, + + 'route' => '/script%3E', + 'paths' => ['script%3E'], + 'params' => null, + 'url' => '/script%3E', + 'environment' => 'unknown', + 'basename' => 'script%3E', + 'base' => 'http://unknown', + 'currentPage' => 1, + 'rootUrl' => 'http://unknown', + 'extension' => null, + 'addNonce' => 'http://unknown/script%3E/nonce:{{nonce}}', + 'toOriginalString' => 'http://unknown/script%3E' + ], + 'http://localhost/">' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/%22%3E%3Cscript%3Ealert%3C/script%3E', + 'query' => '', + 'fragment' => null, + + 'route' => '/%22%3E%3Cscript%3Ealert%3C/script%3E', + 'paths' => ['%22%3E%3Cscript%3Ealert%3C', 'script%3E'], + 'params' => null, + 'url' => '/%22%3E%3Cscript%3Ealert%3C/script%3E', + 'environment' => 'localhost', + 'basename' => 'script%3E', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => null, + 'addNonce' => 'http://localhost/%22%3E%3Cscript%3Ealert%3C/script%3E/nonce:{{nonce}}', + 'toOriginalString' => 'http://localhost/%22%3E%3Cscript%3Ealert%3C/script%3E' + ], + 'http://localhost/something/p1:foo/p2:">' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/something/script%3E', + 'query' => '', + 'fragment' => null, + + 'route' => '/something/script%3E', + 'paths' => ['something', 'script%3E'], + 'params' => '/p1:foo/p2:%22%3E%3Cscript%3Ealert%3C', + 'url' => '/something/script%3E', + 'environment' => 'localhost', + 'basename' => 'script%3E', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => null, + //'addNonce' => 'http://localhost/something/script%3E/p1:foo/p2:%22%3E%3Cscript%3Ealert%3C/nonce:{{nonce}}', // FIXME <- + 'toOriginalString' => 'http://localhost/something/script%3E/p1:foo/p2:%22%3E%3Cscript%3Ealert%3C' + ], + 'http://localhost/something?p=">' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/something', + 'query' => 'p=%22%3E%3Cscript%3Ealert%3C%2Fscript%3E', + 'fragment' => null, + + 'route' => '/something', + 'paths' => ['something'], + 'params' => null, + 'url' => '/something', + 'environment' => 'localhost', + 'basename' => 'something', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => null, + 'addNonce' => 'http://localhost/something/nonce:{{nonce}}?p=%22%3E%3Cscript%3Ealert%3C/script%3E', + 'toOriginalString' => 'http://localhost/something?p=%22%3E%3Cscript%3Ealert%3C/script%3E' + ], + 'http://localhost/something#">' => [ + 'scheme' => 'http://', + 'user' => null, + 'password' => null, + 'host' => 'localhost', + 'port' => 80, + 'path' => '/something', + 'query' => '', + 'fragment' => '%22%3E%3Cscript%3Ealert%3C/script%3E', + + 'route' => '/something', + 'paths' => ['something'], + 'params' => null, + 'url' => '/something', + 'environment' => 'localhost', + 'basename' => 'something', + 'base' => 'http://localhost', + 'currentPage' => 1, + 'rootUrl' => 'http://localhost', + 'extension' => null, + 'addNonce' => 'http://localhost/something/nonce:{{nonce}}#%22%3E%3Cscript%3Ealert%3C/script%3E', + 'toOriginalString' => 'http://localhost/something#%22%3E%3Cscript%3Ealert%3C/script%3E' + ], + 'https://www.getgrav.org/something/"><' => [ + 'scheme' => 'https://', + 'user' => null, + 'password' => null, + 'host' => 'www.getgrav.org', + 'port' => 443, + 'path' => '/something/%22%3E%3Cscript%3Eeval%28atob%28%22aGlzdG9yeS5wdXNoU3RhdGUoJycsJycsJy8nKTskKCdoZWFkLGJvZHknKS5odG1sKCcnKS5sb2FkKCcvJyk7JC5wb3N0KCcvYWRtaW4nLGZ1bmN0aW9uKGRhdGEpeyQucG9zdCgkKGRhdGEpLmZpbmQoJ1tpZD1hZG1pbi11c2VyLWRldGFpbHNdIGEnKS5hdHRyKCdocmVmJykseydhZG1pbi1ub25jZSc6JChkYXRhKS5maW5kKCdbZGF0YS1jbGVhci1jYWNoZV0nKS5hdHRyKCdkYXRhLWNsZWFyLWNhY2hlJykuc3BsaXQoJzonKS5wb3AoKS50cmltKCksJ2RhdGFbcGFzc3dvcmRdJzonSW0zdjFsaDR4eDByJywndGFzayc6J3NhdmUnfSl9KQ==%22%29%29%3C/script%3E%3C', + 'query' => '', + 'fragment' => null, + + 'route' => '/something/%22%3E%3Cscript%3Eeval%28atob%28%22aGlzdG9yeS5wdXNoU3RhdGUoJycsJycsJy8nKTskKCdoZWFkLGJvZHknKS5odG1sKCcnKS5sb2FkKCcvJyk7JC5wb3N0KCcvYWRtaW4nLGZ1bmN0aW9uKGRhdGEpeyQucG9zdCgkKGRhdGEpLmZpbmQoJ1tpZD1hZG1pbi11c2VyLWRldGFpbHNdIGEnKS5hdHRyKCdocmVmJykseydhZG1pbi1ub25jZSc6JChkYXRhKS5maW5kKCdbZGF0YS1jbGVhci1jYWNoZV0nKS5hdHRyKCdkYXRhLWNsZWFyLWNhY2hlJykuc3BsaXQoJzonKS5wb3AoKS50cmltKCksJ2RhdGFbcGFzc3dvcmRdJzonSW0zdjFsaDR4eDByJywndGFzayc6J3NhdmUnfSl9KQ==%22%29%29%3C/script%3E%3C', + 'paths' => ['something', '%22%3E%3Cscript%3Eeval%28atob%28%22aGlzdG9yeS5wdXNoU3RhdGUoJycsJycsJy8nKTskKCdoZWFkLGJvZHknKS5odG1sKCcnKS5sb2FkKCcvJyk7JC5wb3N0KCcvYWRtaW4nLGZ1bmN0aW9uKGRhdGEpeyQucG9zdCgkKGRhdGEpLmZpbmQoJ1tpZD1hZG1pbi11c2VyLWRldGFpbHNdIGEnKS5hdHRyKCdocmVmJykseydhZG1pbi1ub25jZSc6JChkYXRhKS5maW5kKCdbZGF0YS1jbGVhci1jYWNoZV0nKS5hdHRyKCdkYXRhLWNsZWFyLWNhY2hlJykuc3BsaXQoJzonKS5wb3AoKS50cmltKCksJ2RhdGFbcGFzc3dvcmRdJzonSW0zdjFsaDR4eDByJywndGFzayc6J3NhdmUnfSl9KQ==%22%29%29%3C', 'script%3E%3C'], + 'params' => null, + 'url' => '/something/%22%3E%3Cscript%3Eeval%28atob%28%22aGlzdG9yeS5wdXNoU3RhdGUoJycsJycsJy8nKTskKCdoZWFkLGJvZHknKS5odG1sKCcnKS5sb2FkKCcvJyk7JC5wb3N0KCcvYWRtaW4nLGZ1bmN0aW9uKGRhdGEpeyQucG9zdCgkKGRhdGEpLmZpbmQoJ1tpZD1hZG1pbi11c2VyLWRldGFpbHNdIGEnKS5hdHRyKCdocmVmJykseydhZG1pbi1ub25jZSc6JChkYXRhKS5maW5kKCdbZGF0YS1jbGVhci1jYWNoZV0nKS5hdHRyKCdkYXRhLWNsZWFyLWNhY2hlJykuc3BsaXQoJzonKS5wb3AoKS50cmltKCksJ2RhdGFbcGFzc3dvcmRdJzonSW0zdjFsaDR4eDByJywndGFzayc6J3NhdmUnfSl9KQ==%22%29%29%3C/script%3E%3C', + 'environment' => 'www.getgrav.org', + 'basename' => 'script%3E%3C', + 'base' => 'https://www.getgrav.org', + 'currentPage' => 1, + 'rootUrl' => 'https://www.getgrav.org', + 'extension' => null, + 'addNonce' => 'https://www.getgrav.org/something/%22%3E%3Cscript%3Eeval%28atob%28%22aGlzdG9yeS5wdXNoU3RhdGUoJycsJycsJy8nKTskKCdoZWFkLGJvZHknKS5odG1sKCcnKS5sb2FkKCcvJyk7JC5wb3N0KCcvYWRtaW4nLGZ1bmN0aW9uKGRhdGEpeyQucG9zdCgkKGRhdGEpLmZpbmQoJ1tpZD1hZG1pbi11c2VyLWRldGFpbHNdIGEnKS5hdHRyKCdocmVmJykseydhZG1pbi1ub25jZSc6JChkYXRhKS5maW5kKCdbZGF0YS1jbGVhci1jYWNoZV0nKS5hdHRyKCdkYXRhLWNsZWFyLWNhY2hlJykuc3BsaXQoJzonKS5wb3AoKS50cmltKCksJ2RhdGFbcGFzc3dvcmRdJzonSW0zdjFsaDR4eDByJywndGFzayc6J3NhdmUnfSl9KQ==%22%29%29%3C/script%3E%3C/nonce:{{nonce}}', + 'toOriginalString' => 'https://www.getgrav.org/something/%22%3E%3Cscript%3Eeval%28atob%28%22aGlzdG9yeS5wdXNoU3RhdGUoJycsJycsJy8nKTskKCdoZWFkLGJvZHknKS5odG1sKCcnKS5sb2FkKCcvJyk7JC5wb3N0KCcvYWRtaW4nLGZ1bmN0aW9uKGRhdGEpeyQucG9zdCgkKGRhdGEpLmZpbmQoJ1tpZD1hZG1pbi11c2VyLWRldGFpbHNdIGEnKS5hdHRyKCdocmVmJykseydhZG1pbi1ub25jZSc6JChkYXRhKS5maW5kKCdbZGF0YS1jbGVhci1jYWNoZV0nKS5hdHRyKCdkYXRhLWNsZWFyLWNhY2hlJykuc3BsaXQoJzonKS5wb3AoKS50cmltKCksJ2RhdGFbcGFzc3dvcmRdJzonSW0zdjFsaDR4eDByJywndGFzayc6J3NhdmUnfSl9KQ==%22%29%29%3C/script%3E%3C' + ], + ]; + + protected function _before() + { + $grav = Fixtures::get('grav'); + $this->grav = $grav(); + $this->uri = $this->grav['uri']; + } + + protected function _after() + { + } + + protected function runTestSet(array $tests, $method, $params = []) + { + foreach ($tests as $url => $candidates) { + if (!array_key_exists($method, $candidates) && $method !== 'toOriginalString') { + continue; + } + if ($method === 'addNonce') { + $nonce = Utils::getNonce('test-action'); + $expected = str_replace('{{nonce}}', $nonce, $candidates[$method]); + + $this->assertSame($expected, Uri::addNonce($url, 'test-action')); + + continue; + } + + $this->uri->initializeWithURL($url)->init(); + if ($method === 'toOriginalString' && !isset($candidates[$method])) { + $expected = $url; + } else { + $expected = $candidates[$method]; + } + + if ($params) { + $result = call_user_func_array([$this->uri, $method], $params); + } else { + $result = $this->uri->{$method}(); + } + + $this->assertSame($expected, $result, "Test \$url->{$method}() for {$url}"); + // Deal with $url->query($key) + if ($method === 'query') { + parse_str($expected, $queryParams); + foreach ($queryParams as $key => $value) { + $this->assertSame($value, $this->uri->{$method}($key), "Test \$url->{$method}('{$key}') for {$url}"); + } + $this->assertNull($this->uri->{$method}('non-existing'), "Test \$url->{$method}('non-existing') for {$url}"); + } + } + } + + public function testValidatingHostname() + { + $this->assertTrue($this->uri->validateHostname('localhost')); + $this->assertTrue($this->uri->validateHostname('google.com')); + $this->assertTrue($this->uri->validateHostname('google.it')); + $this->assertTrue($this->uri->validateHostname('goog.le')); + $this->assertTrue($this->uri->validateHostname('goog.wine')); + $this->assertTrue($this->uri->validateHostname('goog.localhost')); + + $this->assertFalse($this->uri->validateHostname('localhost:80') ); + $this->assertFalse($this->uri->validateHostname('http://localhost')); + $this->assertFalse($this->uri->validateHostname('localhost!')); + } + + public function testToString() + { + $this->runTestSet($this->tests, 'toOriginalString'); + } + + public function testScheme() + { + $this->runTestSet($this->tests, 'scheme'); + } + + public function testUser() + { + $this->runTestSet($this->tests, 'user'); + } + + public function testPassword() + { + $this->runTestSet($this->tests, 'password'); + } + + public function testHost() + { + $this->runTestSet($this->tests, 'host'); + } + + public function testPort() + { + $this->runTestSet($this->tests, 'port'); + } + + public function testPath() + { + $this->runTestSet($this->tests, 'path'); + } + + public function testQuery() + { + $this->runTestSet($this->tests, 'query'); + } + + public function testFragment() + { + $this->runTestSet($this->tests, 'fragment'); + + $this->uri->fragment('something-new'); + $this->assertSame('something-new', $this->uri->fragment()); + } + + public function testPaths() + { + $this->runTestSet($this->tests, 'paths'); + } + + public function testRoute() + { + $this->runTestSet($this->tests, 'route'); + } + + public function testParams() + { + $this->runTestSet($this->tests, 'params'); + + $this->uri->initializeWithURL('http://localhost:8080/grav/it/ueper:xxx')->init(); + $this->assertSame('/ueper:xxx', $this->uri->params('ueper')); + $this->uri->initializeWithURL('http://localhost:8080/grav/it/ueper:xxx/test:yyy')->init(); + $this->assertSame('/ueper:xxx', $this->uri->params('ueper')); + $this->assertSame('/test:yyy', $this->uri->params('test')); + $this->uri->initializeWithURL('http://localhost:8080/grav/it/ueper:xxx++/test:yyy')->init(); + $this->assertSame('/ueper:xxx++/test:yyy', $this->uri->params()); + $this->assertSame('/ueper:xxx++', $this->uri->params('ueper')); + $this->assertSame('/test:yyy', $this->uri->params('test')); + $this->uri->initializeWithURL('http://localhost:8080/grav/it/ueper:xxx++/test:yyy#something')->init(); + $this->assertSame('/ueper:xxx++/test:yyy', $this->uri->params()); + $this->assertSame('/ueper:xxx++', $this->uri->params('ueper')); + $this->assertSame('/test:yyy', $this->uri->params('test')); + $this->uri->initializeWithURL('http://localhost:8080/grav/it/ueper:xxx++/test:yyy?foo=bar')->init(); + $this->assertSame('/ueper:xxx++/test:yyy', $this->uri->params()); + $this->assertSame('/ueper:xxx++', $this->uri->params('ueper')); + $this->assertSame('/test:yyy', $this->uri->params('test')); + $this->uri->initializeWithURL('http://localhost:8080/grav/it/ueper?test=x')->init(); + $this->assertNull($this->uri->params()); + $this->assertNull($this->uri->params('ueper')); + $this->uri->initializeWithURL('http://localhost:8080/grav/it/ueper?test=x&test2=y')->init(); + $this->assertNull($this->uri->params()); + $this->assertNull($this->uri->params('ueper')); + $this->uri->initializeWithURL('http://localhost:8080/grav/it/ueper?test=x&test2=y&test3=x&test4=y')->init(); + $this->assertNull($this->uri->params()); + $this->assertNull($this->uri->params('ueper')); + $this->uri->initializeWithURL('http://localhost:8080/grav/it/ueper?test=x&test2=y&test3=x&test4=y/test')->init(); + $this->assertNull($this->uri->params()); + $this->assertNull($this->uri->params('ueper')); + $this->uri->initializeWithURL('http://localhost:8080/a/b/c/d')->init(); + $this->assertNull($this->uri->params()); + $this->assertNull($this->uri->params('ueper')); + $this->uri->initializeWithURL('http://localhost:8080/a/b/c/d/e/f/a/b/c/d/e/f/a/b/c/d/e/f')->init(); + $this->assertNull($this->uri->params()); + $this->assertNull($this->uri->params('ueper')); + } + + public function testParam() + { + $this->uri->initializeWithURL('http://localhost:8080/grav/it/ueper:xxx')->init(); + $this->assertSame('xxx', $this->uri->param('ueper')); + $this->uri->initializeWithURL('http://localhost:8080/grav/it/ueper:xxx/test:yyy')->init(); + $this->assertSame('xxx', $this->uri->param('ueper')); + $this->assertSame('yyy', $this->uri->param('test')); + $this->uri->initializeWithURL('http://localhost:8080/grav/it/ueper:xxx++/test:yy%20y/foo:bar_baz-bank')->init(); + $this->assertSame('xxx++', $this->uri->param('ueper')); + $this->assertSame('yy y', $this->uri->param('test')); + $this->assertSame('bar_baz-bank', $this->uri->param('foo')); + } + + public function testUrl() + { + $this->runTestSet($this->tests, 'url'); + } + + public function testExtension() + { + $this->runTestSet($this->tests, 'extension'); + + $this->uri->initializeWithURL('http://localhost/a-page')->init(); + $this->assertSame('x', $this->uri->extension('x')); + } + + public function testEnvironment() + { + $this->runTestSet($this->tests, 'environment'); + } + + public function testBasename() + { + $this->runTestSet($this->tests, 'basename'); + } + + public function testBase() + { + $this->runTestSet($this->tests, 'base'); + } + + public function testRootUrl() + { + $this->runTestSet($this->tests, 'rootUrl', [true]); + + $this->uri->initializeWithUrlAndRootPath('https://localhost/grav/page-foo', '/grav')->init(); + $this->assertSame('/grav', $this->uri->rootUrl()); + $this->assertSame('https://localhost/grav', $this->uri->rootUrl(true)); + } + + public function testCurrentPage() + { + $this->runTestSet($this->tests, 'currentPage'); + + $this->uri->initializeWithURL('http://localhost:8080/a-page/page:2')->init(); + $this->assertSame('2', $this->uri->currentPage()); + } + + public function testReferrer() + { + $this->uri->initializeWithURL('http://localhost/foo/page:test')->init(); + $this->assertSame('/foo', $this->uri->referrer()); + $this->uri->initializeWithURL('http://localhost/foo/bar/page:test')->init(); + $this->assertSame('/foo/bar', $this->uri->referrer()); + } + + public function testIp() + { + $this->uri->initializeWithURL('http://localhost/foo/page:test')->init(); + $this->assertSame('UNKNOWN', Uri::ip()); + } + + public function testIsExternal() + { + $this->uri->initializeWithURL('http://localhost/')->init(); + $this->assertFalse(Uri::isExternal('/test')); + $this->assertFalse(Uri::isExternal('/foo/bar')); + $this->assertTrue(Uri::isExternal('http://localhost/test')); + $this->assertTrue(Uri::isExternal('http://google.it/test')); + } + + public function testBuildUrl() + { + $parsed_url = [ + 'scheme' => 'http', + 'host' => 'localhost', + 'port' => 8080, + ]; + + $this->assertSame('http://localhost:8080', Uri::buildUrl($parsed_url)); + + $parsed_url = [ + 'scheme' => 'http', + 'host' => 'localhost', + 'port' => 8080, + 'user' => 'foo', + 'pass' => 'bar', + 'path' => '/test', + 'query' => 'x=2', + 'fragment' => 'xxx', + ]; + + $this->assertSame('http://foo:bar@localhost:8080/test?x=2#xxx', Uri::buildUrl($parsed_url)); + + /** @var Uri $uri */ + $uri = Grav::instance()['uri']; + + $uri->initializeWithUrlAndRootPath('https://testing.dev/subdir/path1/path2/file.html', '/subdir')->init(); + $this->assertSame('https://testing.dev/subdir/path1/path2/file.html', Uri::buildUrl($uri->toArray(true))); + + $uri->initializeWithUrlAndRootPath('https://testing.dev/subdir/path1/path2/file.foo', '/subdir')->init(); + $this->assertSame('https://testing.dev/subdir/path1/path2/file.foo', Uri::buildUrl($uri->toArray(true))); + + $uri->initializeWithUrlAndRootPath('https://testing.dev/subdir/path1/path2/file.html', '/subdir/path1')->init(); + $this->assertSame('https://testing.dev/subdir/path1/path2/file.html', Uri::buildUrl($uri->toArray(true))); + + $uri->initializeWithUrlAndRootPath('https://testing.dev/subdir/path1/path2/file.html/foo:blah/bang:boom', '/subdir')->init(); + $this->assertSame('https://testing.dev/subdir/path1/path2/file.html/foo:blah/bang:boom', Uri::buildUrl($uri->toArray(true))); + + $uri->initializeWithUrlAndRootPath('https://testing.dev/subdir/path1/path2/file.html/foo:blah/bang:boom?fig=something', '/subdir')->init(); + $this->assertSame('https://testing.dev/subdir/path1/path2/file.html/foo:blah/bang:boom?fig=something', Uri::buildUrl($uri->toArray(true))); + } + + public function testConvertUrl() + { + + } + + public function testAddNonce() + { + $this->runTestSet($this->tests, 'addNonce'); + } +} diff --git a/tests/unit/Grav/Common/UtilsTest.php b/tests/unit/Grav/Common/UtilsTest.php new file mode 100644 index 0000000..e786242 --- /dev/null +++ b/tests/unit/Grav/Common/UtilsTest.php @@ -0,0 +1,526 @@ +grav = $grav(); + $this->uri = $this->grav['uri']; + } + + protected function _after() + { + } + + public function testStartsWith() + { + $this->assertTrue(Utils::startsWith('english', 'en')); + $this->assertTrue(Utils::startsWith('English', 'En')); + $this->assertTrue(Utils::startsWith('ENGLISH', 'EN')); + $this->assertTrue(Utils::startsWith('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', + 'EN')); + + $this->assertFalse(Utils::startsWith('english', 'En')); + $this->assertFalse(Utils::startsWith('English', 'EN')); + $this->assertFalse(Utils::startsWith('ENGLISH', 'en')); + $this->assertFalse(Utils::startsWith('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', + 'e')); + + $this->assertTrue(Utils::startsWith('english', 'En', false)); + $this->assertTrue(Utils::startsWith('English', 'EN', false)); + $this->assertTrue(Utils::startsWith('ENGLISH', 'en', false)); + $this->assertTrue(Utils::startsWith('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', + 'e', false)); + + } + + public function testEndsWith() + { + $this->assertTrue(Utils::endsWith('english', 'sh')); + $this->assertTrue(Utils::endsWith('EngliSh', 'Sh')); + $this->assertTrue(Utils::endsWith('ENGLISH', 'SH')); + $this->assertTrue(Utils::endsWith('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', + 'ENGLISH')); + + $this->assertFalse(Utils::endsWith('english', 'de')); + $this->assertFalse(Utils::endsWith('EngliSh', 'sh')); + $this->assertFalse(Utils::endsWith('ENGLISH', 'Sh')); + $this->assertFalse(Utils::endsWith('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', + 'DEUSTCH')); + + $this->assertTrue(Utils::endsWith('english', 'SH', false)); + $this->assertTrue(Utils::endsWith('EngliSh', 'sH', false)); + $this->assertTrue(Utils::endsWith('ENGLISH', 'sh', false)); + $this->assertTrue(Utils::endsWith('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', + 'english', false)); + } + + public function testContains() + { + $this->assertTrue(Utils::contains('english', 'nglis')); + $this->assertTrue(Utils::contains('EngliSh', 'gliSh')); + $this->assertTrue(Utils::contains('ENGLISH', 'ENGLI')); + $this->assertTrue(Utils::contains('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', + 'ENGLISH')); + + $this->assertFalse(Utils::contains('EngliSh', 'GLI')); + $this->assertFalse(Utils::contains('EngliSh', 'English')); + $this->assertFalse(Utils::contains('ENGLISH', 'SCH')); + $this->assertFalse(Utils::contains('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', + 'DEUSTCH')); + + $this->assertTrue(Utils::contains('EngliSh', 'GLI', false)); + $this->assertTrue(Utils::contains('EngliSh', 'ENGLISH', false)); + $this->assertTrue(Utils::contains('ENGLISH', 'ish', false)); + $this->assertTrue(Utils::contains('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', + 'english', false)); + } + + public function testSubstrToString() + { + $this->assertEquals('en', Utils::substrToString('english', 'glish')); + $this->assertEquals('english', Utils::substrToString('english', 'test')); + $this->assertNotEquals('en', Utils::substrToString('english', 'lish')); + + $this->assertEquals('en', Utils::substrToString('english', 'GLISH', false)); + $this->assertEquals('english', Utils::substrToString('english', 'TEST', false)); + $this->assertNotEquals('en', Utils::substrToString('english', 'LISH', false)); + } + + public function testMergeObjects() + { + $obj1 = new stdClass(); + $obj1->test1 = 'x'; + $obj2 = new stdClass(); + $obj2->test2 = 'y'; + + $objMerged = Utils::mergeObjects($obj1, $obj2); + + $this->assertObjectHasAttribute('test1', $objMerged); + $this->assertObjectHasAttribute('test2', $objMerged); + } + + public function testDateFormats() + { + $dateFormats = Utils::dateFormats(); + $this->assertInternalType('array', $dateFormats); + $this->assertContainsOnly('string', $dateFormats); + + $default_format = $this->grav['config']->get('system.pages.dateformat.default'); + + if ($default_format !== null) { + $this->assertArrayHasKey($default_format, $dateFormats); + } + } + + public function testTruncate() + { + $this->assertEquals('engli' . '…', Utils::truncate('english', 5)); + $this->assertEquals('english', Utils::truncate('english')); + $this->assertEquals('This is a string to truncate', Utils::truncate('This is a string to truncate')); + $this->assertEquals('Th' . '…', Utils::truncate('This is a string to truncate', 2)); + $this->assertEquals('engli' . '...', Utils::truncate('english', 5, true, " ", "...")); + $this->assertEquals('english', Utils::truncate('english')); + $this->assertEquals('This is a string to truncate', Utils::truncate('This is a string to truncate')); + $this->assertEquals('This' . '…', Utils::truncate('This is a string to truncate', 3, true)); + $this->assertEquals('', 6, true)); + + } + + public function testSafeTruncate() + { + $this->assertEquals('This' . '…', Utils::safeTruncate('This is a string to truncate', 1)); + $this->assertEquals('This' . '…', Utils::safeTruncate('This is a string to truncate', 4)); + $this->assertEquals('This is' . '…', Utils::safeTruncate('This is a string to truncate', 5)); + } + + public function testTruncateHtml() + { + $this->assertEquals('T...', Utils::truncateHtml('This is a string to truncate', 1)); + $this->assertEquals('This is...', Utils::truncateHtml('This is a string to truncate', 7)); + $this->assertEquals('

    T...

    ', Utils::truncateHtml('

    This is a string to truncate

    ', 1)); + $this->assertEquals('

    This...

    ', Utils::truncateHtml('

    This is a string to truncate

    ', 4)); + $this->assertEquals('

    This is a...

    ', Utils::truncateHtml('

    This is a string to truncate

    ', 10)); + $this->assertEquals('

    This is a string to truncate

    ', Utils::truncateHtml('

    This is a string to truncate

    ', 100)); + $this->assertEquals('', Utils::truncateHtml('', 6)); + $this->assertEquals('
    1. item 1 so...
    ', Utils::truncateHtml('
    1. item 1 something
    2. item 2 bold
    ', 10)); + $this->assertEquals("

    This is a string.

    \n

    It splits two lines.

    ", Utils::truncateHtml("

    This is a string.

    \n

    It splits two lines.

    ", 100)); + } + + public function testSafeTruncateHtml() + { + $this->assertEquals('This...', Utils::safeTruncateHtml('This is a string to truncate', 1)); + $this->assertEquals('This is a...', Utils::safeTruncateHtml('This is a string to truncate', 3)); + $this->assertEquals('

    This...

    ', Utils::safeTruncateHtml('

    This is a string to truncate

    ', 1)); + $this->assertEquals('

    This is...

    ', Utils::safeTruncateHtml('

    This is a string to truncate

    ', 2)); + $this->assertEquals('

    This is a string to...

    ', Utils::safeTruncateHtml('

    This is a string to truncate

    ', 5)); + $this->assertEquals('

    This is a string to truncate

    ', Utils::safeTruncateHtml('

    This is a string to truncate

    ', 20)); + $this->assertEquals('', Utils::safeTruncateHtml('', 6)); + $this->assertEquals('
    1. item 1 something
    2. item 2...
    ', Utils::safeTruncateHtml('
    1. item 1 something
    2. item 2 bold
    ', 5)); + } + + public function testGenerateRandomString() + { + $this->assertNotEquals(Utils::generateRandomString(), Utils::generateRandomString()); + $this->assertNotEquals(Utils::generateRandomString(20), Utils::generateRandomString(20)); + } + + public function download() + { + + } + + public function testGetMimeByExtension() + { + $this->assertEquals('application/octet-stream', Utils::getMimeByExtension('')); + $this->assertEquals('text/html', Utils::getMimeByExtension('html')); + $this->assertEquals('application/json', Utils::getMimeByExtension('json')); + $this->assertEquals('application/atom+xml', Utils::getMimeByExtension('atom')); + $this->assertEquals('application/rss+xml', Utils::getMimeByExtension('rss')); + $this->assertEquals('image/jpeg', Utils::getMimeByExtension('jpg')); + $this->assertEquals('image/png', Utils::getMimeByExtension('png')); + $this->assertEquals('text/plain', Utils::getMimeByExtension('txt')); + $this->assertEquals('application/msword', Utils::getMimeByExtension('doc')); + $this->assertEquals('application/octet-stream', Utils::getMimeByExtension('foo')); + $this->assertEquals('foo/bar', Utils::getMimeByExtension('foo', 'foo/bar')); + $this->assertEquals('text/html', Utils::getMimeByExtension('foo', 'text/html')); + } + + public function testGetExtensionByMime() + { + $this->assertEquals('html', Utils::getExtensionByMime('*/*')); + $this->assertEquals('html', Utils::getExtensionByMime('text/*')); + $this->assertEquals('html', Utils::getExtensionByMime('text/html')); + $this->assertEquals('json', Utils::getExtensionByMime('application/json')); + $this->assertEquals('atom', Utils::getExtensionByMime('application/atom+xml')); + $this->assertEquals('rss', Utils::getExtensionByMime('application/rss+xml')); + $this->assertEquals('jpg', Utils::getExtensionByMime('image/jpeg')); + $this->assertEquals('png', Utils::getExtensionByMime('image/png')); + $this->assertEquals('txt', Utils::getExtensionByMime('text/plain')); + $this->assertEquals('doc', Utils::getExtensionByMime('application/msword')); + $this->assertEquals('html', Utils::getExtensionByMime('foo/bar')); + $this->assertEquals('baz', Utils::getExtensionByMime('foo/bar', 'baz')); + } + + public function testNormalizePath() + { + $this->assertEquals('/test', Utils::normalizePath('/test')); + $this->assertEquals('test', Utils::normalizePath('test')); + $this->assertEquals('test', Utils::normalizePath('../test')); + $this->assertEquals('/test', Utils::normalizePath('/../test')); + $this->assertEquals('/test2', Utils::normalizePath('/test/../test2')); + $this->assertEquals('/test3', Utils::normalizePath('/test/../test2/../test3')); + + $this->assertEquals('//cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css', Utils::normalizePath('//cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css')); + $this->assertEquals('//use.fontawesome.com/releases/v5.8.1/css/all.css', Utils::normalizePath('//use.fontawesome.com/releases/v5.8.1/css/all.css')); + $this->assertEquals('//use.fontawesome.com/releases/v5.8.1/webfonts/fa-brands-400.eot', Utils::normalizePath('//use.fontawesome.com/releases/v5.8.1/css/../webfonts/fa-brands-400.eot')); + + $this->assertEquals('http://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css', Utils::normalizePath('http://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css')); + $this->assertEquals('http://use.fontawesome.com/releases/v5.8.1/css/all.css', Utils::normalizePath('http://use.fontawesome.com/releases/v5.8.1/css/all.css')); + $this->assertEquals('http://use.fontawesome.com/releases/v5.8.1/webfonts/fa-brands-400.eot', Utils::normalizePath('http://use.fontawesome.com/releases/v5.8.1/css/../webfonts/fa-brands-400.eot')); + + $this->assertEquals('https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css', Utils::normalizePath('https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css')); + $this->assertEquals('https://use.fontawesome.com/releases/v5.8.1/css/all.css', Utils::normalizePath('https://use.fontawesome.com/releases/v5.8.1/css/all.css')); + $this->assertEquals('https://use.fontawesome.com/releases/v5.8.1/webfonts/fa-brands-400.eot', Utils::normalizePath('https://use.fontawesome.com/releases/v5.8.1/css/../webfonts/fa-brands-400.eot')); + } + + public function testIsFunctionDisabled() + { + $disabledFunctions = explode(',', ini_get('disable_functions')); + + if ($disabledFunctions[0]) { + $this->assertEquals(Utils::isFunctionDisabled($disabledFunctions[0]), true); + } + } + + public function testTimezones() + { + $timezones = Utils::timezones(); + + $this->assertInternalType('array', $timezones); + $this->assertContainsOnly('string', $timezones); + } + + public function testArrayFilterRecursive() + { + $array = [ + 'test' => '', + 'test2' => 'test2' + ]; + + $array = Utils::arrayFilterRecursive($array, function ($k, $v) { + return !(is_null($v) || $v === ''); + }); + + $this->assertContainsOnly('string', $array); + $this->assertArrayNotHasKey('test', $array); + $this->assertArrayHasKey('test2', $array); + $this->assertEquals('test2', $array['test2']); + } + + public function testPathPrefixedByLangCode() + { + $languagesEnabled = $this->grav['config']->get('system.languages.supported', []); + $arrayOfLanguages = ['en', 'de', 'it', 'es', 'dk', 'el']; + $languagesNotEnabled = array_diff($arrayOfLanguages, $languagesEnabled); + $oneLanguageNotEnabled = reset($languagesNotEnabled); + + if (count($languagesEnabled)) { + $this->assertTrue(Utils::pathPrefixedByLangCode('/' . $languagesEnabled[0] . '/test')); + } + + $this->assertFalse(Utils::pathPrefixedByLangCode('/' . $oneLanguageNotEnabled . '/test')); + $this->assertFalse(Utils::pathPrefixedByLangCode('/test')); + $this->assertFalse(Utils::pathPrefixedByLangCode('/xx')); + $this->assertFalse(Utils::pathPrefixedByLangCode('/xx/')); + $this->assertFalse(Utils::pathPrefixedByLangCode('/')); + } + + public function testDate2timestamp() + { + $timestamp = strtotime('10 September 2000'); + $this->assertSame($timestamp, Utils::date2timestamp('10 September 2000')); + $this->assertSame($timestamp, Utils::date2timestamp('2000-09-10 00:00:00')); + } + + public function testResolve() + { + $array = [ + 'test' => [ + 'test2' => 'test2Value' + ] + ]; + + $this->assertEquals('test2Value', Utils::resolve($array, 'test.test2')); + } + + public function testGetDotNotation() + { + $array = [ + 'test' => [ + 'test2' => 'test2Value', + 'test3' => [ + 'test4' => 'test4Value' + ] + ] + ]; + + $this->assertEquals('test2Value', Utils::getDotNotation($array, 'test.test2')); + $this->assertEquals('test4Value', Utils::getDotNotation($array, 'test.test3.test4')); + $this->assertEquals('defaultValue', Utils::getDotNotation($array, 'test.non_existent', 'defaultValue')); + } + + public function testSetDotNotation() + { + $array = [ + 'test' => [ + 'test2' => 'test2Value', + 'test3' => [ + 'test4' => 'test4Value' + ] + ] + ]; + + $new = [ + 'test1' => 'test1Value' + ]; + + Utils::setDotNotation($array, 'test.test3.test4' , $new); + $this->assertEquals('test1Value', $array['test']['test3']['test4']['test1']); + } + + public function testIsPositive() + { + $this->assertTrue(Utils::isPositive(true)); + $this->assertTrue(Utils::isPositive(1)); + $this->assertTrue(Utils::isPositive('1')); + $this->assertTrue(Utils::isPositive('yes')); + $this->assertTrue(Utils::isPositive('on')); + $this->assertTrue(Utils::isPositive('true')); + $this->assertFalse(Utils::isPositive(false)); + $this->assertFalse(Utils::isPositive(0)); + $this->assertFalse(Utils::isPositive('0')); + $this->assertFalse(Utils::isPositive('no')); + $this->assertFalse(Utils::isPositive('off')); + $this->assertFalse(Utils::isPositive('false')); + $this->assertFalse(Utils::isPositive('some')); + $this->assertFalse(Utils::isPositive(2)); + } + + public function testGetNonce() + { + $this->assertInternalType('string', Utils::getNonce('test-action')); + $this->assertInternalType('string', Utils::getNonce('test-action', true)); + $this->assertSame(Utils::getNonce('test-action'), Utils::getNonce('test-action')); + $this->assertNotSame(Utils::getNonce('test-action'), Utils::getNonce('test-action2')); + } + + public function testVerifyNonce() + { + $this->assertTrue(Utils::verifyNonce(Utils::getNonce('test-action'), 'test-action')); + } + + public function testUrl() + { + $this->uri->initializeWithUrl('http://testing.dev/path1/path2')->init(); + + // Fail hard + $this->assertSame(false, Utils::url('', true)); + $this->assertSame(false, Utils::url('')); + $this->assertSame(false, Utils::url(new stdClass())); + $this->assertSame(false, Utils::url(['foo','bar','baz'])); + $this->assertSame(false, Utils::url('user://does/not/exist')); + + // Fail Gracefully + $this->assertSame('/', Utils::url('/', false, true)); + $this->assertSame('/', Utils::url('', false, true)); + $this->assertSame('/', Utils::url(new stdClass(), false, true)); + $this->assertSame('/', Utils::url(['foo','bar','baz'], false, true)); + $this->assertSame('/user/does/not/exist', Utils::url('user://does/not/exist', false, true)); + + // Simple paths + $this->assertSame('/', Utils::url('/')); + $this->assertSame('/path1', Utils::url('/path1')); + $this->assertSame('/path1/path2', Utils::url('/path1/path2')); + $this->assertSame('/random/path1/path2', Utils::url('/random/path1/path2')); + $this->assertSame('/foobar.jpg', Utils::url('/foobar.jpg')); + $this->assertSame('/path1/foobar.jpg', Utils::url('/path1/foobar.jpg')); + $this->assertSame('/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg')); + $this->assertSame('/random/path1/path2/foobar.jpg', Utils::url('/random/path1/path2/foobar.jpg')); + + // Simple paths with domain + $this->assertSame('http://testing.dev/', Utils::url('/', true)); + $this->assertSame('http://testing.dev/path1', Utils::url('/path1', true)); + $this->assertSame('http://testing.dev/path1/path2', Utils::url('/path1/path2', true)); + $this->assertSame('http://testing.dev/random/path1/path2', Utils::url('/random/path1/path2', true)); + $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('/foobar.jpg', true)); + $this->assertSame('http://testing.dev/path1/foobar.jpg', Utils::url('/path1/foobar.jpg', true)); + $this->assertSame('http://testing.dev/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg', true)); + $this->assertSame('http://testing.dev/random/path1/path2/foobar.jpg', Utils::url('/random/path1/path2/foobar.jpg', true)); + + // Relative paths from Grav root. + $this->assertSame('/subdir', Utils::url('subdir')); + $this->assertSame('/subdir/path1', Utils::url('subdir/path1')); + $this->assertSame('/subdir/path1/path2', Utils::url('subdir/path1/path2')); + $this->assertSame('/path1', Utils::url('path1')); + $this->assertSame('/path1/path2', Utils::url('path1/path2')); + $this->assertSame('/foobar.jpg', Utils::url('foobar.jpg')); + $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('foobar.jpg', true)); + + // Relative paths from Grav root with domain. + $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('foobar.jpg', true)); + $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('/foobar.jpg', true)); + $this->assertSame('http://testing.dev/path1/foobar.jpg', Utils::url('/path1/foobar.jpg', true)); + + // All Non-existing streams should be treated as external URI / protocol. + $this->assertSame('http://domain.com/path', Utils::url('http://domain.com/path')); + $this->assertSame('ftp://domain.com/path', Utils::url('ftp://domain.com/path')); + $this->assertSame('sftp://domain.com/path', Utils::url('sftp://domain.com/path')); + $this->assertSame('ssh://domain.com', Utils::url('ssh://domain.com')); + $this->assertSame('pop://domain.com', Utils::url('pop://domain.com')); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz')); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz', true)); + // $this->assertSame('mailto:joe@domain.com', Utils::url('mailto:joe@domain.com', true)); // FIXME <- + } + + public function testUrlWithRoot() + { + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/path1/path2', '/subdir')->init(); + + // Fail hard + $this->assertSame(false, Utils::url('', true)); + $this->assertSame(false, Utils::url('')); + $this->assertSame(false, Utils::url(new stdClass())); + $this->assertSame(false, Utils::url(['foo','bar','baz'])); + $this->assertSame(false, Utils::url('user://does/not/exist')); + + // Fail Gracefully + $this->assertSame('/subdir/', Utils::url('/', false, true)); + $this->assertSame('/subdir/', Utils::url('', false, true)); + $this->assertSame('/subdir/', Utils::url(new stdClass(), false, true)); + $this->assertSame('/subdir/', Utils::url(['foo','bar','baz'], false, true)); + $this->assertSame('/subdir/user/does/not/exist', Utils::url('user://does/not/exist', false, true)); + + // Simple paths + $this->assertSame('/subdir/', Utils::url('/')); + $this->assertSame('/subdir/path1', Utils::url('/path1')); + $this->assertSame('/subdir/path1/path2', Utils::url('/path1/path2')); + $this->assertSame('/subdir/random/path1/path2', Utils::url('/random/path1/path2')); + $this->assertSame('/subdir/foobar.jpg', Utils::url('/foobar.jpg')); + $this->assertSame('/subdir/path1/foobar.jpg', Utils::url('/path1/foobar.jpg')); + $this->assertSame('/subdir/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg')); + $this->assertSame('/subdir/random/path1/path2/foobar.jpg', Utils::url('/random/path1/path2/foobar.jpg')); + + // Simple paths with domain + $this->assertSame('http://testing.dev/subdir/', Utils::url('/', true)); + $this->assertSame('http://testing.dev/subdir/path1', Utils::url('/path1', true)); + $this->assertSame('http://testing.dev/subdir/path1/path2', Utils::url('/path1/path2', true)); + $this->assertSame('http://testing.dev/subdir/random/path1/path2', Utils::url('/random/path1/path2', true)); + $this->assertSame('http://testing.dev/subdir/foobar.jpg', Utils::url('/foobar.jpg', true)); + $this->assertSame('http://testing.dev/subdir/path1/foobar.jpg', Utils::url('/path1/foobar.jpg', true)); + $this->assertSame('http://testing.dev/subdir/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg', true)); + $this->assertSame('http://testing.dev/subdir/random/path1/path2/foobar.jpg', Utils::url('/random/path1/path2/foobar.jpg', true)); + + // Paths including the grav base. + $this->assertSame('/subdir/', Utils::url('/subdir')); + $this->assertSame('/subdir/path1', Utils::url('/subdir/path1')); + $this->assertSame('/subdir/path1/path2', Utils::url('/subdir/path1/path2')); + $this->assertSame('/subdir/foobar.jpg', Utils::url('/subdir/foobar.jpg')); + $this->assertSame('/subdir/path1/foobar.jpg', Utils::url('/subdir/path1/foobar.jpg')); + + // Relative paths from Grav root with domain. + $this->assertSame('http://testing.dev/subdir/', Utils::url('/subdir', true)); + $this->assertSame('http://testing.dev/subdir/path1', Utils::url('/subdir/path1', true)); + $this->assertSame('http://testing.dev/subdir/path1/path2', Utils::url('/subdir/path1/path2', true)); + $this->assertSame('http://testing.dev/subdir/foobar.jpg', Utils::url('/subdir/foobar.jpg', true)); + $this->assertSame('http://testing.dev/subdir/path1/foobar.jpg', Utils::url('/subdir/path1/foobar.jpg', true)); + + // Relative paths from Grav root. + $this->assertSame('/subdir/subdir', Utils::url('subdir')); + $this->assertSame('/subdir/subdir/path1', Utils::url('subdir/path1')); + $this->assertSame('/subdir/subdir/path1/path2', Utils::url('subdir/path1/path2')); + $this->assertSame('/subdir/path1', Utils::url('path1')); + $this->assertSame('/subdir/path1/path2', Utils::url('path1/path2')); + $this->assertSame('/subdir/foobar.jpg', Utils::url('foobar.jpg')); + $this->assertSame('http://testing.dev/subdir/foobar.jpg', Utils::url('foobar.jpg', true)); + + // All Non-existing streams should be treated as external URI / protocol. + $this->assertSame('http://domain.com/path', Utils::url('http://domain.com/path')); + $this->assertSame('ftp://domain.com/path', Utils::url('ftp://domain.com/path')); + $this->assertSame('sftp://domain.com/path', Utils::url('sftp://domain.com/path')); + $this->assertSame('ssh://domain.com', Utils::url('ssh://domain.com')); + $this->assertSame('pop://domain.com', Utils::url('pop://domain.com')); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz')); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz', true)); + // $this->assertSame('mailto:joe@domain.com', Utils::url('mailto:joe@domain.com', true)); // FIXME <- + } + + public function testUrlWithStreams() + { + + } + + public function testUrlwithExternals() + { + $this->uri->initializeWithUrl('http://testing.dev/path1/path2')->init(); + $this->assertSame('http://foo.com', Utils::url('http://foo.com')); + $this->assertSame('https://foo.com', Utils::url('https://foo.com')); + $this->assertSame('//foo.com', Utils::url('//foo.com')); + $this->assertSame('//foo.com?param=x', Utils::url('//foo.com?param=x')); + } +} diff --git a/tests/unit/Grav/Console/Gpm/InstallCommandTest.php b/tests/unit/Grav/Console/Gpm/InstallCommandTest.php new file mode 100644 index 0000000..e76843b --- /dev/null +++ b/tests/unit/Grav/Console/Gpm/InstallCommandTest.php @@ -0,0 +1,28 @@ +grav = Fixtures::get('grav'); + $this->installCommand = new InstallCommand(); + + } + + protected function _after() + { + } +} \ No newline at end of file diff --git a/tests/unit/Grav/Framework/File/Formatter/CsvFormatterTest.php b/tests/unit/Grav/Framework/File/Formatter/CsvFormatterTest.php new file mode 100644 index 0000000..70a8c9e --- /dev/null +++ b/tests/unit/Grav/Framework/File/Formatter/CsvFormatterTest.php @@ -0,0 +1,47 @@ + 1, 'col2' => 2, 'col3' => 3], + ['col1' => 'aaa', 'col2' => 'bbb', 'col3' => 'ccc'], + ]; + + $encoded = (new CsvFormatter())->encode($data); + + $lines = array_filter(explode(PHP_EOL, $encoded)); + + self::assertCount(3, $lines); + self::assertEquals('col1,col2,col3', $lines[0]); + + } + + /** + * TBD - If indexes are all numeric, what's the purpose + * of displaying header + */ + public function testEncodeWithIndexColumns() + { + $data = [ + [0 => 1, 1 => 2, 2 => 3], + ]; + + $encoded = (new CsvFormatter())->encode($data); + + $lines = array_filter(explode(PHP_EOL, $encoded)); + + self::assertCount(2, $lines); + self::assertEquals('0,1,2', $lines[0]); + } + + public function testEncodeEmptyData() + { + $encoded = (new CsvFormatter())->encode([]); + self::assertEquals('', $encoded); + } + +} diff --git a/tests/unit/Grav/Framework/Filesystem/FilesystemTest.php b/tests/unit/Grav/Framework/Filesystem/FilesystemTest.php new file mode 100644 index 0000000..132aeb9 --- /dev/null +++ b/tests/unit/Grav/Framework/Filesystem/FilesystemTest.php @@ -0,0 +1,322 @@ + [ + 'parent' => '', + 'normalize' => '', + 'dirname' => '', + 'pathinfo' => [ + 'basename' => '', + 'filename' => '', + ] + ], + '.' => [ + 'parent' => '', + 'normalize' => '', + 'dirname' => '.', + 'pathinfo' => [ + 'dirname' => '.', + 'basename' => '.', + 'extension' => '', + 'filename' => '', + ] + ], + './' => [ + 'parent' => '', + 'normalize' => '', + 'dirname' => '.', + 'pathinfo' => [ + 'dirname' => '.', + 'basename' => '.', + 'extension' => '', + 'filename' => '', + + ] + ], + '././.' => [ + 'parent' => '', + 'normalize' => '', + 'dirname' => './.', + 'pathinfo' => [ + 'dirname' => './.', + 'basename' => '.', + 'extension' => '', + 'filename' => '', + ] + ], + '.file' => [ + 'parent' => '.', + 'normalize' => '.file', + 'dirname' => '.', + 'pathinfo' => [ + 'dirname' => '.', + 'basename' => '.file', + 'extension' => 'file', + 'filename' => '', + ] + ], + '/' => [ + 'parent' => '', + 'normalize' => '/', + 'dirname' => '/', + 'pathinfo' => [ + 'dirname' => '/', + 'basename' => '', + 'filename' => '', + ] + ], + '/absolute' => [ + 'parent' => '/', + 'normalize' => '/absolute', + 'dirname' => '/', + 'pathinfo' => [ + 'dirname' => '/', + 'basename' => 'absolute', + 'filename' => 'absolute', + ] + ], + '/absolute/' => [ + 'parent' => '/', + 'normalize' => '/absolute', + 'dirname' => '/', + 'pathinfo' => [ + 'dirname' => '/', + 'basename' => 'absolute', + 'filename' => 'absolute', + ] + ], + '/very/long/absolute/path' => [ + 'parent' => '/very/long/absolute', + 'normalize' => '/very/long/absolute/path', + 'dirname' => '/very/long/absolute', + 'pathinfo' => [ + 'dirname' => '/very/long/absolute', + 'basename' => 'path', + 'filename' => 'path', + ] + ], + 'relative' => [ + 'parent' => '.', + 'normalize' => 'relative', + 'dirname' => '.', + 'pathinfo' => [ + 'dirname' => '.', + 'basename' => 'relative', + 'filename' => 'relative', + ] + ], + 'very/long/relative/path' => [ + 'parent' => 'very/long/relative', + 'normalize' => 'very/long/relative/path', + 'dirname' => 'very/long/relative', + 'pathinfo' => [ + 'dirname' => 'very/long/relative', + 'basename' => 'path', + 'filename' => 'path', + ] + ], + 'path/to/file.jpg' => [ + 'parent' => 'path/to', + 'normalize' => 'path/to/file.jpg', + 'dirname' => 'path/to', + 'pathinfo' => [ + 'dirname' => 'path/to', + 'basename' => 'file.jpg', + 'extension' => 'jpg', + 'filename' => 'file', + ] + ], + 'user://' => [ + 'parent' => '', + 'normalize' => 'user://', + 'dirname' => 'user://', + 'pathinfo' => [ + 'dirname' => 'user://', + 'basename' => '', + 'filename' => '', + 'scheme' => 'user', + ] + ], + 'user://.' => [ + 'parent' => '', + 'normalize' => 'user://', + 'dirname' => 'user://', + 'pathinfo' => [ + 'dirname' => 'user://', + 'basename' => '', + 'filename' => '', + 'scheme' => 'user', + ] + ], + 'user://././.' => [ + 'parent' => '', + 'normalize' => 'user://', + 'dirname' => 'user://', + 'pathinfo' => [ + 'dirname' => 'user://', + 'basename' => '', + 'filename' => '', + 'scheme' => 'user', + ] + ], + 'user://./././file' => [ + 'parent' => 'user://', + 'normalize' => 'user://file', + 'dirname' => 'user://', + 'pathinfo' => [ + 'dirname' => 'user://', + 'basename' => 'file', + 'filename' => 'file', + 'scheme' => 'user', + ] + ], + 'user://./././folder/file' => [ + 'parent' => 'user://folder', + 'normalize' => 'user://folder/file', + 'dirname' => 'user://folder', + 'pathinfo' => [ + 'dirname' => 'user://folder', + 'basename' => 'file', + 'filename' => 'file', + 'scheme' => 'user', + ] + ], + 'user://.file' => [ + 'parent' => 'user://', + 'normalize' => 'user://.file', + 'dirname' => 'user://', + 'pathinfo' => [ + 'dirname' => 'user://', + 'basename' => '.file', + 'extension' => 'file', + 'filename' => '', + 'scheme' => 'user', + ] + ], + 'user:///' => [ + 'parent' => '', + 'normalize' => 'user:///', + 'dirname' => 'user:///', + 'pathinfo' => [ + 'dirname' => 'user:///', + 'basename' => '', + 'filename' => '', + 'scheme' => 'user', + ] + ], + 'user:///absolute' => [ + 'parent' => 'user:///', + 'normalize' => 'user:///absolute', + 'dirname' => 'user:///', + 'pathinfo' => [ + 'dirname' => 'user:///', + 'basename' => 'absolute', + 'filename' => 'absolute', + 'scheme' => 'user', + ] + ], + 'user:///very/long/absolute/path' => [ + 'parent' => 'user:///very/long/absolute', + 'normalize' => 'user:///very/long/absolute/path', + 'dirname' => 'user:///very/long/absolute', + 'pathinfo' => [ + 'dirname' => 'user:///very/long/absolute', + 'basename' => 'path', + 'filename' => 'path', + 'scheme' => 'user', + ] + ], + 'user://relative' => [ + 'parent' => 'user://', + 'normalize' => 'user://relative', + 'dirname' => 'user://', + 'pathinfo' => [ + 'dirname' => 'user://', + 'basename' => 'relative', + 'filename' => 'relative', + 'scheme' => 'user', + ] + ], + 'user://very/long/relative/path' => [ + 'parent' => 'user://very/long/relative', + 'normalize' => 'user://very/long/relative/path', + 'dirname' => 'user://very/long/relative', + 'pathinfo' => [ + 'dirname' => 'user://very/long/relative', + 'basename' => 'path', + 'filename' => 'path', + 'scheme' => 'user', + ] + ], + 'user://path/to/file.jpg' => [ + 'parent' => 'user://path/to', + 'normalize' => 'user://path/to/file.jpg', + 'dirname' => 'user://path/to', + 'pathinfo' => [ + 'dirname' => 'user://path/to', + 'basename' => 'file.jpg', + 'extension' => 'jpg', + 'filename' => 'file', + 'scheme' => 'user', + ] + ], + ]; + + protected function _before() + { + $this->class = Filesystem::getInstance(); + } + + protected function _after() + { + unset($this->class); + } + + protected function runTestSet(array $tests, $method) + { + $class = $this->class; + foreach ($tests as $path => $candidates) { + if (!array_key_exists($method, $candidates)) { + continue; + } + + $expected = $candidates[$method]; + + $result = $class->{$method}($path); + + $this->assertSame($expected, $result, "Test {$method}('{$path}')"); + + if (function_exists($method) && !strpos($path, '://')) { + $cmp_result = $method($path); + + $this->assertSame($cmp_result, $result, "Compare to original {$method}('{$path}')"); + } + } + } + + public function testParent() + { + $this->runTestSet($this->tests, 'parent'); + } + + public function testNormalize() + { + $this->runTestSet($this->tests, 'normalize'); + } + + public function testDirname() + { + $this->runTestSet($this->tests, 'dirname'); + } + + public function testPathinfo() + { + $this->runTestSet($this->tests, 'pathinfo'); + } +} diff --git a/tests/unit/_bootstrap.php b/tests/unit/_bootstrap.php new file mode 100644 index 0000000..f92ef0c --- /dev/null +++ b/tests/unit/_bootstrap.php @@ -0,0 +1,4 @@ +': gt + '<': lt + types: + - html + - htm + - xml + - txt + - json + - rss + - atom + expires: 604800 + last_modified: false + etag: false + vary_accept_encoding: false + redirect_default_route: false + redirect_default_code: '302' + redirect_trailing_slash: true + ignore_files: + - .DS_Store + ignore_folders: + - .git + - .idea + ignore_hidden: true + url_taxonomy_filters: true + frontmatter: + process_twig: false + ignore_fields: + - form + - forms +cache: + enabled: false + check: + method: file + driver: auto + prefix: g + purge_at: '0 4 * * *' + clear_at: '0 3 * * *' + clear_job_type: standard + clear_images_by_default: false + cli_compatibility: false + lifetime: 0 + gzip: false + allow_webserver_gzip: false +twig: + cache: false + debug: false + auto_reload: true + autoescape: false + undefined_functions: true + undefined_filters: true + umask_fix: false +assets: + css_pipeline: false + css_pipeline_include_externals: false + css_pipeline_before_excludes: false + css_minify: false + css_minify_windows: false + css_rewrite: false + js_pipeline: false + js_pipeline_include_externals: false + js_pipeline_before_excludes: false + js_minify: false + enable_asset_timestamp: false + collections: + jquery: 'system://assets/jquery/jquery-2.x.min.js' +errors: + display: 1 + log: true +log: + handler: file + syslog: + facility: local6 +debugger: + enabled: false + provider: debugbar + shutdown: + close_connection: false + twig: true +images: + default_image_quality: 85 + cache_all: false + cache_perms: '0755' + debug: false + auto_fix_orientation: false + seofriendly: false +media: + enable_media_timestamp: false + auto_metadata_exif: false + upload_limit: 2097152 +session: + enabled: true + initialize: true + timeout: 1800 + name: grav-site + uniqueness: path + secure: false + httponly: true + split: true +gpm: + releases: stable + method: auto + verify_peer: true + official_gpm_only: true +accounts: + type: data + storage: file +strict_mode: + yaml_compat: true + twig_compat: true + blueprint_compat: true diff --git a/user/config/versions.yaml b/user/config/versions.yaml new file mode 100644 index 0000000..4cc09f2 --- /dev/null +++ b/user/config/versions.yaml @@ -0,0 +1,8 @@ +core: + grav: + version: 1.7.35 + history: + - { version: 1.7.14, date: '2021-05-12 08:31:39' } + - { version: 1.7.25, date: '2021-11-19 16:29:05' } + - { version: 1.7.35, date: '2022-09-06 00:16:02' } + schema: 1.7.0_2020-11-20_1 diff --git a/user/data/.gitkeep b/user/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/user/localhost/config/plugins/aura.yaml b/user/localhost/config/plugins/aura.yaml new file mode 100644 index 0000000..576c601 --- /dev/null +++ b/user/localhost/config/plugins/aura.yaml @@ -0,0 +1,16 @@ +enabled: true +output-sd: true +output-og: true +output-twitter: true +output-linkedin: true +org-name: 'Figures Libres' +org-url: figureslibres.cc/ +org-facebook-url: 'https://www.facebook.com/Figureslibres' +org-facebook-appid: null +org-twitter-user: null +org-instagram-url: 'https://www.instagram.com/figureslibres/' +org-linkedin-url: null +org-pinterest-url: null +org-youtube-url: null +org-wikipedia-url: null +org-logo: { } diff --git a/user/localhost/config/plugins/automagic-images.yaml b/user/localhost/config/plugins/automagic-images.yaml new file mode 100644 index 0000000..9c63c90 --- /dev/null +++ b/user/localhost/config/plugins/automagic-images.yaml @@ -0,0 +1,23 @@ +enabled: true +adapter: imagick +remove_original: false +sizes: + - + width: 1000 + quality: 90 + - + width: 1500 + quality: 87 + - + width: 2500 + quality: 85 + - + width: 3500 + quality: 82 +sizesattr: + - + class: default + directive: 100vw + - + class: example + directive: '(min-width: 20em) 33%, 100vw' diff --git a/user/localhost/config/plugins/lazy-image.yaml b/user/localhost/config/plugins/lazy-image.yaml new file mode 100644 index 0000000..95f6491 --- /dev/null +++ b/user/localhost/config/plugins/lazy-image.yaml @@ -0,0 +1,2 @@ +enabled: false +lazy_img_class: null diff --git a/user/localhost/config/site.yaml b/user/localhost/config/site.yaml new file mode 100644 index 0000000..2bcc433 --- /dev/null +++ b/user/localhost/config/site.yaml @@ -0,0 +1,19 @@ +title: 'Figures Libres' +default_lang: fr +author: + name: 'Joe Bloggs' + email: hello@figureslibres.io +taxonomies: + - category + - tag +metadata: + description: 'Figures Libres est un collectif de design graphique et interactif porteur de messages d''utilité publique, sociale et culturelle qui œuvre sur logiciels libres.' +summary: + enabled: true + format: short + size: 300 + delimiter: '===' +redirects: null +routes: null +blog: + route: /blog diff --git a/user/localhost/config/system.yaml b/user/localhost/config/system.yaml new file mode 100644 index 0000000..248dfad --- /dev/null +++ b/user/localhost/config/system.yaml @@ -0,0 +1,228 @@ +absolute_urls: false +timezone: null +param_sep: ':' +wrapped_site: false +reverse_proxy_setup: false +force_ssl: false +force_lowercase_urls: true +custom_base_url: null +username_regex: '^[a-z0-9_-]{3,16}$' +pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}' +intl_enabled: true +http_x_forwarded: + protocol: true + host: false + port: true + ip: true +languages: + supported: null + default_lang: null + include_default_lang: true + include_default_lang_file_extension: true + translations: true + translations_fallback: true + session_store_active: false + http_accept_language: false + override_locale: false + pages_fallback_only: false +home: + alias: /home + hide_in_urls: false +pages: + type: regular + theme: figureslibres-v2 + order: + by: default + dir: asc + list: + count: 20 + dateformat: + default: null + short: 'jS M Y' + long: 'F jS \a\t g:ia' + publish_dates: true + process: + markdown: true + twig: false + twig_first: false + never_cache_twig: false + events: + page: true + twig: true + markdown: + extra: false + auto_line_breaks: false + auto_url_links: false + escape_markup: false + special_chars: + '>': gt + '<': lt + valid_link_attributes: + - rel + - target + - id + - class + - classes + types: + - html + - htm + - xml + - txt + - json + - rss + - atom + append_url_extension: null + expires: 604800 + cache_control: null + last_modified: false + etag: false + vary_accept_encoding: false + redirect_default_code: '302' + redirect_trailing_slash: 1 + redirect_default_route: 0 + ignore_files: + - .DS_Store + ignore_folders: + - .git + - .idea + ignore_hidden: true + hide_empty_folders: false + url_taxonomy_filters: true + frontmatter: + process_twig: false + ignore_fields: + - form + - forms +cache: + enabled: false + check: + method: file + driver: auto + prefix: g + purge_at: '0 4 * * *' + clear_at: '0 3 * * *' + clear_job_type: standard + clear_images_by_default: false + cli_compatibility: false + lifetime: 0 + gzip: false + allow_webserver_gzip: false + redis: + socket: '0' + password: null + database: null + server: null + port: null + memcache: + server: null + port: null + memcached: + server: null + port: null +twig: + cache: false + debug: true + auto_reload: true + autoescape: false + undefined_functions: true + undefined_filters: true + safe_functions: { } + safe_filters: { } + umask_fix: false +assets: + css_pipeline: false + css_pipeline_include_externals: false + css_pipeline_before_excludes: false + css_minify: false + css_minify_windows: false + css_rewrite: false + js_pipeline: false + js_pipeline_include_externals: false + js_pipeline_before_excludes: false + js_minify: false + enable_asset_timestamp: false + enable_asset_sri: false + collections: + jquery: 'system://assets/jquery/jquery-2.x.min.js' +errors: + display: 1 + log: true +log: + handler: file + syslog: + facility: local6 +debugger: + enabled: true + provider: debugbar + censored: false + shutdown: + close_connection: false + twig: true +images: + default_image_quality: 85 + cache_all: false + cache_perms: '0755' + debug: false + auto_fix_orientation: false + seofriendly: false + cls: + auto_sizes: false + aspect_ratio: false + retina_scale: '1' + defaults: + loading: auto + watermark: + image: 'system://images/watermark.png' + position_y: center + position_x: center + scale: 33 + watermark_all: false +media: + enable_media_timestamp: false + unsupported_inline_types: null + allowed_fallback_types: null + auto_metadata_exif: false + upload_limit: 2097152 +session: + enabled: true + initialize: true + timeout: 1800 + name: grav-site + uniqueness: path + secure: false + httponly: true + samesite: Lax + split: true + domain: null + path: null +gpm: + releases: stable + official_gpm_only: true + method: auto + verify_peer: true +http: + method: auto + enable_proxy: true + proxy_url: null + proxy_cert_path: null + concurrent_connections: 5 + verify_peer: true + verify_host: true +accounts: + type: regular + storage: file +flex: + cache: + index: + enabled: true + lifetime: 60 + object: + enabled: true + lifetime: 600 + render: + enabled: true + lifetime: 600 +strict_mode: + yaml_compat: true + twig_compat: true + blueprint_compat: true diff --git a/user/plugins/.gitkeep b/user/plugins/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/user/plugins/admin/.editorconfig b/user/plugins/admin/.editorconfig new file mode 100644 index 0000000..3b95e6d --- /dev/null +++ b/user/plugins/admin/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +# 2 space indentation +[*.{yaml,.yml}] +indent_size = 2 diff --git a/user/plugins/admin/.gitattributes b/user/plugins/admin/.gitattributes new file mode 100644 index 0000000..c5ef731 --- /dev/null +++ b/user/plugins/admin/.gitattributes @@ -0,0 +1,8 @@ +# Linguist Normalizer +*.yaml linguistic-language=PHP +*.twig linguistic-language=PHP +**/gulpfile.babel.js linguist-vendored +**/webpack.conf.js linguist-vendored +**/js/*.js linguist-vendored +**/js/*.json linguist-vendored +**/css-compiled/*.css linguist-vendored diff --git a/user/plugins/admin/.github/FUNDING.yml b/user/plugins/admin/.github/FUNDING.yml new file mode 100644 index 0000000..e84f52b --- /dev/null +++ b/user/plugins/admin/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: grav +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: # Replace with a single custom sponsorship URL diff --git a/user/plugins/admin/.gitignore b/user/plugins/admin/.gitignore new file mode 100644 index 0000000..77d3944 --- /dev/null +++ b/user/plugins/admin/.gitignore @@ -0,0 +1,16 @@ +themes/grav/.sass-cache +.DS_Store +crowdin.yaml + +# Node Modules +**/node_modules/** +themes/grav/js/admin.js +themes/grav/js/vendor.js +themes/grav/js/*.map +.idea + +tests/_output/* +tests/_support/_generated/* +tests/cache/* +tests/error.log +/crowdin.yaml diff --git a/user/plugins/admin/CHANGELOG.md b/user/plugins/admin/CHANGELOG.md new file mode 100644 index 0000000..d6fea9a --- /dev/null +++ b/user/plugins/admin/CHANGELOG.md @@ -0,0 +1,2427 @@ +# v1.10.25 +## 11/16/2021 + +3. [](#bugfix) + * Fixed unescaped messages in JSON responses + +# v1.10.24 +## 10/26/2021 + +1. [](#new) + * Require **Grav 1.7.24** +2. [](#improved) + * Use new `Http\Response` rather than deprecated `GPM\Response` +3. [](#bugfix) + * Fixed an issue with invalid HTML throwing errors on HTML security scanning + * Clear cache when installing plugins + +# v1.10.23 +## 09/29/2021 + +1. [](#new) + * Updated SCSS compiler to v1.8 +2. [](#improved) + * Updated with latest language strings from Crowdin.com +3. [](#bugfix) + * Fixed images from plugins/themes disappearing when saving twice + +# v1.10.22 +## 09/16/2021 + +1. [](#new) + * Updated SCSS compiler to v1.7 + +# v1.10.21 +## 09/14/2021 + +1. [](#new) + * Require **Grav 1.7.21** +2. [](#improved) + * Added a note about UTC times in scheduler AT syntax help + * Now using a monospaced text-based scheduler AT field in scheduler for simplicity + * Improved `Admin:data()` and `Admin::getConfigurationData()` to be more strict +3. [](#bugfix) + * Fixed configuration save location to point to existing config folder [#2176](https://github.com/getgrav/grav-plugin-admin/issues/2176) + +# v1.10.20 +## 09/01/2021 + +1. [](#bugfix) + * Fixed regression `Argument 4 passed to Grav\Plugin\Form\TwigExtension::prepareFormField() must be of the type array` [#2177](https://github.com/getgrav/grav-plugin-admin/issues/2177) + * Fixed `X-Frame-Options` to be `DENY` in all admin pages to prevent a clickjacking attack + +# v1.10.19 +## 08/31/2021 + +1. [](#new) + * Require **Grav 1.7.19** and **Form 5.1.0** and **Login 3.5.0** + * Updated SCSS compiler to v1.6 +2. [](#improved) + * Updated forms and nested fields to use new form logic + * Admin form now use layout `admin`, meaning you can create admin specific field templates by `forms/fields/myfield/admin-field.html.twig` + * Stop using `|tu` filter, Grav already has the same logic in `|t` for admin + * Remove unneeded escapes + * Allow removal of plugin when disabled [#2167](https://github.com/getgrav/grav-plugin-admin/issues/2167) +3. [](#bugfix) + * Fixed missing values in `fieldset` form field + +# v1.10.18 +## 07/19/2021 + +1. [](#improved) + * Add logic to allow fieldset form field inside a list field [#2159](https://github.com/getgrav/grav-plugin-admin/pull/2159) + +# v1.10.17 +## 06/15/2021 + +1. [](#improved) + * Added timestamp as title in logs date [#2141](https://github.com/getgrav/grav-plugin-admin/issues/2141) + * Use `base64_encode` filter rather than function + * Composer update +1. [](#bugfix) + * Fixed missing `Remove Theme` button when the theme is inactive + * Update taskGetChildTypes() to use Flex Pages (works without the plugin) [#2087](https://github.com/getgrav/grav-plugin-admin/issues/2087) + +# v1.10.16 +## 06/02/2021 + +1. [](#bugfix) + * Fixed issue with some elements overflowing closed list items [#2146](https://github.com/getgrav/grav-plugin-admin/issues/2146) + * Fixed configuration not fully updating on save [#2149](https://github.com/getgrav/grav-plugin-admin/issues/2149) + * Fixed display issue with "+ Add Page" and picking a different route [#2136](https://github.com/getgrav/grav-plugin-admin/issues/2136), [#2145](https://github.com/getgrav/grav-plugin-admin/issues/2145) + * Treat WebP as image when inserting / drag & dropping [#2150](https://github.com/getgrav/grav-plugin-admin/issues/2150) + +# v1.10.15 +## 05/19/2021 + +1. [](#new) + * Updated SCSS compiler to v1.5 +1. [](#improved) + * Updated node modules dev dependencies + * Package.json scripts cleanup + * Recompiled JS for production + * Use `base645_encode` filter rather than function + * Editor: Do not assume images URLs are going to be `http://` (wrong assumption plus not SSL) [#2127](https://github.com/getgrav/grav-plugin-admin/issues/2127) + * Improved Theme Activation + Plugin Enabled logic to ensure configuration is not displayed unless activation/enabled state. Fixes [#2140](https://github.com/getgrav/grav-plugin-admin/issues/2140) +1. [](#bugfix) + * Fixed issue with slugify where single curly quotes in titles would translate to straight single quote [#2101](https://github.com/getgrav/grav-plugin-admin/issues/2101) + * Fix z-index issue with fullscreeen editor (and toolips) [#2143](https://github.com/getgrav/grav-plugin-admin/issues/2143) + +# v1.10.14 +## 04/29/2021 + +1. [](#improved) + * Added a `min_height:` option for list field +1. [](#bugfix) + * Fixed z-index issue for tooltips in sidebar + * Fixed custom files being overridden during theme update [#2135](https://github.com/getgrav/grav-plugin-admin/issues/2135) + +# v1.10.13 +## 04/23/2021 + +1. [](#new) + * Added refresh action button for Folder to ease the regeneration of the slug based on the title. Available also as API entry `Grav.default.Forms.Fields.FolderField.Regenerate()` [#1738](https://github.com/getgrav/grav-plugin-admin/issues/1738) +1. [](#improved) + * Removed sourcemaps references from fork-awesome.min.css [#2122](https://github.com/getgrav/grav-plugin-admin/issues/2122) + * Support native spell checkers in CodeMirror editor [#1266](https://github.com/getgrav/grav-plugin-admin/issues/1266) + * Added new 'Content Highlight' color to presets + * Copying Pages now prompts a dedicated modal that allows for picking title, folder name, parent location, page template and visibility [#1738](https://github.com/getgrav/grav-plugin-admin/issues/1738) + * Updated with latest language translations from Crowdin.com +1. [](#bugfix) + * Moved preset CSS compile to earlier in the process to ensure compilation happens in time. + * Prevent Save actions from Flex Objects to trigger the unsaved unload notice [#2125](https://github.com/getgrav/grav-plugin-admin/issues/2125) + * Fixed audit vulnerabilities in module dependencies and house cleanup [#2096](https://github.com/getgrav/grav-plugin-admin/issues/2096) + * Fixed issue preventing Drag & Drop of media files while in Expert Mode [#1927](https://github.com/getgrav/grav-plugin-admin/issues/1927) + * Fixed broken link colors in `preset.css` which was causing issues with tabs and dropdowns + * Fixed permissions for page related tasks and actions + * Fixed permission check for configuration save [#2130](https://github.com/getgrav/grav-plugin-admin/issues/2130) + * Fixed missing/wrong page categories and tags when multi-language support is enabled [#2107](https://github.com/getgrav/grav-plugin-admin/issues/2107) + +# v1.10.12 +## 04/15/2021 + +1. [](#bugfix) + * Regression: Fixed broken plugin/theme installer in admin + * Fixed error reporting for AJAX tasks if user has no permissions + * Fixed missing slash in password reset URL [#2119](https://github.com/getgrav/grav-plugin-admin/issues/2119) + +# v1.10.11 +## 04/13/2021 + +1. [](#bugfix) + * **IMPORTANT** Fixed security vulnerability that allows installation of plugins with minimal admin privileges [GHSA-wg37-cf5x-55hq](https://github.com/getgrav/grav-plugin-admin/security/advisories/GHSA-wg37-cf5x-55hq) + * Fixed `You have been logged out` message when entering to 2FA authentication due to `/admin/task:getNotifications` AJAX call + * Fixed broken 2FA login when site is not configured to use Flex Users [#2109](https://github.com/getgrav/grav-plugin-admin/issues/2109) + * Fixed error message when user clicks logout link after the session has been expired + +# v1.10.10 +## 04/07/2021 + +1. [](#bugfix) + * Fixed missing `admin-preset.css` in multisite environments + * Regression: Fixed broken 2FA form [#2109](https://github.com/getgrav/grav-plugin-admin/issues/2109) + +# v1.10.9 +## 04/06/2021 + +1. [](#new) + * Requires **Grav 1.7.10** +1. [](#improved) + * Better isolate admin to prevent session related vulnerabilities + * Removed support for custom login redirects for improved security + * Shorten forgot password link lifetime from 7 days to 1 hour + * Updated with latest language translations from Crowdin.com +1. [](#bugfix) + * Fixed issue where Adding a new page and canceling from within Editing would alter the Parent location of the edited page [#2067](https://github.com/getgrav/grav-plugin-admin/issues/2067) + * Fixed and enhanced Range field to be Lists compatible [#2062](https://github.com/getgrav/grav-plugin-admin/issues/2062) + * Fixed ERR_TOO_MANY_REDIRECTS with HTTPS = 'On' [#2100](https://github.com/getgrav/grav-plugin-admin/issues/2100) + * Prevent expert editing mode from anyone else than super users [#2094](https://github.com/getgrav/grav-plugin-admin/issues/2094) + * Fixed login related pages being accessible from admin when user has logged in + * Fixed admin user creation and password reset allowing unsafe passwords + * Fixed missing validation when registering the first admin user + * Fixed reset password email not to have session specific token in it + * Fixed admin controller running before setting `$grav['page']` + +# v1.10.8 +## 03/19/2021 + +1. [](#improved) + * Include alt text and title for images added to the editor [#2098](https://github.com/getgrav/grav-plugin-admin/issues/2098) +1. [](#bugfix) + * Fixed issue replacing `wildcard` field names in flex collections [#2092](https://github.com/getgrav/grav-plugin-admin/pull/2092) + * Fixed legacy Pages having old `modular` reference instead of `module` [#2093](https://github.com/getgrav/grav-plugin-admin/issues/2093) + * Fixed issue where Add New modal would close if selecting an item outside of the modal window. It is now necessary go through the Cancel button and clicking the overlay won't trigger the closing of the modal [#2089](https://github.com/getgrav/grav-plugin-admin/issues/2089), [#2065](https://github.com/getgrav/grav-plugin-admin/issues/2065) + +# v1.10.7 +## 03/17/2021 + +1. [](#improved) + * Force height of Flex pages admin to fit available space + * Updated languages from Crowdin.com + * Better field type definitions for file, pagemedia, filepicker and pagemediafield +1. [](#bugfix) + * Fixed error when checking missing log file [#2088](https://github.com/getgrav/grav-plugin-admin/issues/2088) + +# v1.10.6 +## 02/23/2021 + +1. [](#new) + * Vastly improved support for RTL languages [#2078](https://github.com/getgrav/grav-plugin-admin/pull/2078) +1. [](#improved) + * Flex pages admin better uses available space [#2075](https://github.com/getgrav/grav/issues/2075) +1. [](#bugfix) + * Regression: Fixed enabling/disabling plugin or theme corrupting configuration + * Fixed unnecessary closing bracket causing JS error [#2079](https://github.com/getgrav/grav-plugin-admin/issues/2079) + * Fixed wrong language in Admin Tools [#2077](https://github.com/getgrav/grav-plugin-admin/issues/2077) + +# v1.10.5 +## 02/18/2021 + +1. [](#bugfix) + * Regression: Fixed fatal error in admin if POST request has `data` in it [#2074](https://github.com/getgrav/grav-plugin-admin/issues/2074) + * Fixed Admin creating empty `user/config/info.yaml` file (the file can be safely removed, it is not in use) + * Fixed ACL for users with mixed case usernames [#2073](https://github.com/getgrav/grav-plugin-admin/issues/2073) + +# v1.10.4 +## 02/17/2021 + +1. [](#new) + * Added support to include new page creation modals in other pages by using `form_action` twig variable [#2024](https://github.com/getgrav/grav-plugin-admin/pull/2024) + * Updated all languages from [Crowdin](https://crowdin.com/project/grav-admin) - Please update any translations here +1. [](#improved) + * Removed `noscript` template, because 2021... + * List field: added new `placement` property to decide wether to add new items at the top, bottom or based on the *position* of the clicked button [#2055](https://github.com/getgrav/grav-plugin-admin/pull/2055) + * Ensure admin default CSS styles load **first**, and presets loads **last** + * Tweaked handling of uploaded files [#1429](https://github.com/getgrav/grav-plugin-admin/issues/1429) + * Provide media object and filename in `onAdminAfterDelMedia` event [#1905](https://github.com/getgrav/grav-plugin-admin/pull/1905) +1. [](#bugfix) + * Fixed case-sensitive `accept` in `filepicker` field + * Fixed HTML Entities in titles [#2028](https://github.com/getgrav/grav-plugin-admin/issues/2028) + * Fixed deleting list field options completely, didn't save changes [#2056](https://github.com/getgrav/grav-plugin-admin/issues/2056) + * Fixed `onAdminAfterAddMedia` and `onAdminAfterDelMedia` events always pointing to the home page + * Fixed ACL for Configuration tabs [#771](https://github.com/getgrav/grav-plugin-admin/issues/771) + * Fixed changelog button showing up in Info page even if user cannot access it + * Fixed toggleable checkboxes being unchecked in fieldset columns [#2063](https://github.com/getgrav/grav-plugin-admin/issues/2063) + * Fixed issue with max backups of zero [#2070](https://github.com/getgrav/grav-plugin-admin/issues/2070) + +# v1.10.3 +## 02/01/2021 + +1. [](#new) + * Requires **Grav 1.7.4** (SemVer library moved to Grav) + * Added back special fonts (including Gantry) +2. [](#bugfix) + * Fixed field type `range` not taking into account legitimate `0` values + * Fixed `Call to a member function trackHit() on null` [#2049](https://github.com/getgrav/grav-plugin-admin/issues/2049) + +# v1.10.2 +## 01/21/2021 + +2. [](#bugfix) + * Fixed admin style compilation failing to save CSS if assets folder does not exist + +# v1.10.1 +## 01/20/2021 + +1. [](#improved) + * Added `watch.sh` for compiling SCSS with native sass compiler +2. [](#bugfix) + * Fixed issue with overlapping sidebar when using fullscreen editor [#2022](https://github.com/getgrav/grav-plugin-admin/issues/2022) + +# v1.10.0 +## 01/19/2021 + +1. [](#new) + * Requires **Grav 1.7 and PHP 7.3.6** + * Read about this release in the [Grav 1.7 Released](https://getgrav.org/blog/grav-1.7-released) blog post + * Read the full list of changes in the [Changelog on GitHub](https://github.com/getgrav/grav-plugin-admin/blob/1.10.0/CHANGELOG.md) + * Please read [Grav 1.7 Upgrade Guide](https://learn.getgrav.org/17/advanced/grav-development/grav-17-upgrade-guide) before upgrading! +1. [](#improved) + * Various notifications improvements +1. [](#bugfix) + * Fixed missed highlight on the selected page in Parents field + * Fixed notifications that would not be remembered as hidden + * Fixed taxonomy field not listing existing options in Flex Pages + * Fixed taxonomy field not working outside pages + * Fixed fatal error when moving a page using the old implementation [#2019](https://github.com/getgrav/grav-plugin-admin/issues/2019) + * Fixed evaluating default value in `hidden` field (thanks @NicoHood) + +# v1.10.0-rc.20 +## 12/14/2020 + +1. [](#improved) + * Cookies now explicitly set `SameSite` to `Lax` unless otherwise specified [#1998](https://github.com/getgrav/grav-plugin-admin/issues/1998) + * Exposed **Cookies** class (`Grav.default.Utils.Cookies`) for developers that need it in Admin. +1. [](#bugfix) + * Fixed Plugins references in Themes details page. + * Fixed issue preventing purchase of Themes within Admin and redirecting instead. + * Regression: Values inside Fieldset do not display [#1995](https://github.com/getgrav/grav-plugin-admin/issues/1995) + +# v1.10.0-rc.19 +## 12/02/2020 + +1. [](#improved) + * Just keeping sync with Grav rc.19 + +# v1.10.0-rc.18 +## 12/02/2020 + +1. [](#new) + * Retired "Secure Delete" and "Warn on page delete". You are now always warned and asked to confirm a deletion. +1. [](#improved) + * Auto-link a plugin/theme license in details if it starts with `http` + * Allow to fallback to `docs:` instead of `readme:` + * Forward a `sid` to GPM when downloading a premium package + * Better support for array field key/value when either key or value is stored empty [#1972](https://github.com/getgrav/grav-plugin-admin/issues/1972) + * Remember the open state of the sidebar [#1973](https://github.com/getgrav/grav-plugin-admin/issues/1973) + * Upgraded node dependencies to latest version. Improved speed of JS compilation. + * Added modal to confirm updating Grav as well as cool down counter before enabling Update button [#1257](https://github.com/getgrav/grav-plugin-admin/issues/1257) + * Better handling of offline/intranet mode when the repository index is missing. Faster admin. [#1916](https://github.com/getgrav/grav-plugin-admin/issues/1916) + * Statistics is now Page View Statistics [#1885](https://github.com/getgrav/grav-plugin-admin/issues/1885) + * It is now possible to use regex as values for "Hide page types in Admin" and "Hide modular page types in Admin" settings [#1828](https://github.com/getgrav/grav-plugin-admin/issues/1828) + * Default to `disabled` state for all cron-jobs +1. [](#bugfix) + * Fixed Safari issue with new ACL picker field [#1955](https://github.com/getgrav/grav-plugin-admin/issues/1955) + * Stop propagation of ACL add button in ACL picker [flex-objects#83](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/83) + * Fixed missing special groups `authors` and `defaults` for pages + * Fixed Page Move action and selection highlight in Parents selector modal [flex-objects#80](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/80) + * Fixed folder auto-naming in Add Module [#1937](https://github.com/getgrav/grav-plugin-admin/issues/1937) + * Fixed remodal issue triggering close when selecting a dropdown item ending outside of scope [#1682](https://github.com/getgrav/grav-plugin-admin/issues/1682) + * Reworked how collapsed lists work so the tooltip is not cut off [#1928](https://github.com/getgrav/grav-plugin-admin/issues/1928) + * Fixed KeepAlive issue where too large of a session value would fire the keep alive immediately [#1860](https://github.com/getgrav/grav-plugin-admin/issues/1860) + * Fixed stringable objects breaking the inputs + * Fixed filepicker, pagemediaselect fields with `multiple: true` and `array: true` [#1580](https://github.com/getgrav/grav-plugin-admin/issues/1580) + +# v1.10.0-rc.17 +## 10/07/2020 + +1. [](#new) + * Support premium themes +1. [](#improved) + * Improved some error messages for better readability + * Strip tags from browser title +1. [](#bugfix) + * More multi-site routing fixes + * Fixed issue that would force a page reload when failing to install/update a plugin or theme. + * Fixed proxy/browser caching issues in admin pages + +# v1.10.0-rc.16 +## 09/01/2020 + +1. [](#improved) + * Made all the `onAdmin*` CRUD events to pass `object` (and backwards compatible `page`) to make them easier to use + * Updated vendor libraries including `SCSSPHP` to v1.2 +1. [](#bugfix) + * Fixed issue with File field being used in Theme/Plugins + * Fixed bad redirection after successful admin login in subdirectory multisite [#1487](https://github.com/getgrav/grav-plugin-admin/issues/1487) + +# v1.10.0-rc.15 +## 07/22/2020 + +1. [](#bugfix) + * Disabled the EXIF library for Dropzone for fixing the orientation as it was getting applied twice [#1923](https://github.com/getgrav/grav-plugin-admin/issues/1923) + * Forked Dropzone fo fix issue with Resize + Exif orientation [#1923](https://github.com/getgrav/grav-plugin-admin/issues/1923) + * Fixed URI encode for the preview of images names + +# v1.10.0-rc.14 +## 07/09/2020 + +1. [](#improved) + * Completely removed old Google font support for upgrade compatibility +1. [](#bugfix) + * Fixed bad `use` reference to `UserObject` + +# v1.10.0-rc.13 +## 07/01/2020 + +1. [](#improved) + * Improved color picker field + * Trim login route for safety + * Composer update to grab latest vendor libs + +# v1.10.0-rc.12 +## 06/08/2020 + +1. [](#new) + * Added ability to set a preferred markdown editor in user profile + * Added new `onAdminListContentEditors` event to add a custom editor to the list of available +1. [](#bugfix) + * Fixed issue deleting file from a plugin's configuration + * Use `Pages::find()` instead of `Pages::dispatch()` as we do not want to redirect out of admin + * Fixed broken `parent` field when using the old pages + * Fixed broken `file` field preview when using streams in the path + +# v1.10.0-rc.11 +## 05/14/2020 + +1. [](#new) + * Major enhancements to "White Label" functionality including ability to export/import presets + * New horizontal scroller for theme presets + * Codemirror Fontsize / Preset / Font preference options +1. [](#improved) + * Fixed lots of styling issues related to "White Label" presets + * Changed out "One Light" theme for new "Firewatch Light" theme + * New scrolling system based on `SimpleBar` + native CSS scrollbar styling + +# v1.10.0-rc.10 +## 04/30/2020 + +1. [](#new) + * Addd new `taskConvertUrls` method for use with 3rd party editors + +# v1.10.0-rc.9 +## 04/27/2020 + +1. [](#new) + * Added new "White Label" functionality to customize admin colors + logos + * Added badge count for children in the Parents field +1. [](#improved) + * Added markdown support to `text` in `section` field +1. [](#bugfix) + * Prevent loading Pages in Parents field if they don't have children + * Fixed custom folder in `mediapicker` field not working with streams + * Fixed language redirect adding extra language prefix in Flex + * Fixed `Invalid input in "Parent"` when saving page in raw mode [#1869](https://github.com/getgrav/grav-plugin-admin/issues/1869) + +# v1.10.0-rc.8 +## 03/19/2020 + +1. [](#new) + * Added `has-children` flag in parent field data response + * Added `RESET` en lang string +1. [](#bugfix) + * Fixed parent field not working with regular pages + +# v1.10.0-rc.7 +## 03/05/2020 + +1. [](#new) + * Enable admin cache by default (for existing sites, check `Plugins > Admin Panel > Enable Admin Caching`) +1. [](#improved) + * Removed old `scss.sh` and `watch.sh` scripts, use `gulp watch-css` + * Added keysOnly parameter to `AdminPlugin::pagesTypes()` and `AdminPlugin::pagesModularTypes()` methods + * Added ignore parameter to `Admin::types()` and `Admin::modularTypes()` methods + * Improved configuration fields for hiding page types in Admin +1. [](#bugfix) + * Fixed minor UI padding in Flex pages [#1825](https://github.com/getgrav/grav-plugin-admin/issues/1825) + * Fixed `column` and `section` fields loosing user entered value when form submit fails + * Fixed `order` field not working with a new Flex Page + +# v1.10.0-rc.6 +## 02/11/2020 + +1. [](#new) + * Pass phpstan level 1 tests + * Updated semver library to v1.5 + * Require flex-objects plugin +1. [](#improved) + * Added some debugging messages (turned off by default) + +# v1.10.0-rc.5 +## 02/03/2020 + +1. [](#new) + * No changes, just keeping things in sync with Grav RC version + +# v1.10.0-rc.4 +## 02/03/2020 + +1. [](#new) + * Added message to dashboard to install Flex Objects plugin if it is missing + * Updated `permissions` field to use new `$grav['permissions']` + * DEPRECATED `onAdminRegisterPermissions` event, use `PermissionsRegisterEvent::class` event instead + * DEPRECATED `Admin::setPermissions()` and `Admin::addPermissions()`, use `PermissionsRegisterEvent::class` event instead + * DEPRECATED `Admin::getPermissions()`, use `$grav['permissions']->getInstances()` instead +1. [](#improved) + * Added `field.show_label` and `field.label` display logic from frontend forms +1. [](#bugfix) + * Fixed user profile when using `Flex Users` only in admin + * Fixed saving data with empty field, default value (from config, plugin, theme) was used instead + * Fixed JS bug is using empty Grav URI param key + * Fixed bug in toggleable field being disabled with empty value (`''` `0`, `false`, `[]`...) + * Fixed `admin_route()` twig function to work properly with Grav 1.7.0-rc.4, which fixes `Route` base + * Fixed misleading 'Show sensitive data' configuration option wording [#1818](https://github.com/getgrav/grav-plugin-admin/issues/1818) + +# v1.10.0-rc.3 +## 01/02/2020 + +1. [](#new) + * Added ability to display **Changelogs** for `Grav`, `Plugins` and `Themes` + * Added raw root page support for `Flex Pages` + +# v1.10.0-rc.2 +## 12/04/2019 + +1. [](#new) + * Added support for hiding parts of admin by `Deny` permissions (`Flex Users` only) + * Optimized `parent` field for Flex Pages +1. [](#improved) + * Improved `permissions` field to add support for displaying calculated permissions + * Grav 1.7: Updated deprecated `$page->modular()` method calls to `$page->isModule()` + * Output the current process user name in Scheduler instructions + * Translations: rename MODULAR to MODULE everywhere +1. [](#bugfix) + * Fixed `permissions` field with nested permissions + * Fixed Save Shortcut (CTRL + S / CMD + S) not working with new Flex Pages [#1787](https://github.com/getgrav/grav-plugin-admin/issues/1787) + +# v1.10.0-rc.1 +## 11/06/2019 + +1. [](#new) + * Added a new `onAdminLogFiles()` event for 3rd party plugins to register log files for log viewer [#1765](https://github.com/getgrav/grav-plugin-admin/issues/1765) +1. [](#improved) + * Improved delete button UI [#1769](https://github.com/getgrav/grav-plugin-admin/issues/1769) + * Ability to configure display of 3rd party dashboard widgets [#1766](https://github.com/getgrav/grav-plugin-admin/issues/1766) +1. [](#bugfix) + * Fixed administrator user creation when `Flex Users` is enabled + * Fixed minor button alignment in FF [#1760](https://github.com/getgrav/grav-plugin-admin/issues/1760) + +# v1.10.0-beta.10 +## 10/03/2019 + +1. [](#bugfix) + * Regression: Fixed language assignments for the pages without set language + +# v1.10.0-beta.9 +## 09/26/2019 + +1. [](#bugfix) + * Make pages field to work with Flex Pages + +# v1.10.0-beta.8 +## 09/19/2019 + +1. [](#new) + * Add ability to Sanitize SVGs on file upload + * Add ability to Sanitize SVGs in Page media +1. [](#improved) + * YAML linter report now supports multi-language + * Better colors/placement of toolbar buttons in page edit view +1. [](#bugfix) + * Fixed missing language for AJAX requests + * Fixed redirect with absolute language URL + * Fixed issue with user avatar reference not being deleted when image removed + +# v1.10.0-beta.7 +## 08/30/2019 + +1. [](#bugfix) + * Fixed regression: Do not require Flex Objects plugin [grav#2653](https://github.com/getgrav/grav/issues/2653) + +# v1.10.0-beta.6 +## 08/29/2019 + +1. [](#improved) + * Optimized admin for speed (only load frontend pages on demand) + * Updated navigation menu to be fully controlled and overrideable by `onAdminMenu` event + * Lots of Flex Page speed improvements + +# v1.10.0-beta.5 +## 08/11/2019 + +1. [](#new) + * Added `data()` twig function to create data object from an array +1. [](#improved) + * Better support for `array` field into `list` field + * Made RAW blueprints (expert mode) to work properly with Flex Form + * Better support for `clockwork` logs +1. [](#bugfix) + * Fixed issue with nested `list` fields both utilizing the custom `key` functionality + * Regression: Page Preview not working, bad url [#1715](https://github.com/getgrav/grav-plugin-admin/issues/1715) + * Fixed '+New Folder' to work with new parent picker + * Fixed missing XSS check field when editing modular page as raw + * Fixed minor CSS layout issue [#1717](https://github.com/getgrav/grav-plugin-admin/issues/1717) + +# v1.10.0-beta.4 +## 07/01/2019 + +1. [](#new) + * Added `Admin::redirect()` method to allow redirects to be used outside of controllers + * Added `$admin->adminRoute()` method and `admin_route()` twig function to create language aware admin page links + * Renamed `Admin::route()` to `Admin::getCurrentRoute()` and deprecated the old call +1. [](#improved) + * Much improved multi-language support for pages + * Admin redirects should now work better with multiple languages enabled +1. [](#bugfix) + * Fixed default language being renamed to `page.en.md` (English) instead of keeping existing `page.md` filename + * Fixed possibility to override already existing translation by `Save As Language` + * Fixed missing default translation if page used plain `.md` file extension without language code + * Fixed wrong translation showing up as page fallback language + * Integrated Admin 1.9.8 bug fixes + +# v1.10.0-beta.3 +## 06/24/2019 + +1. [](#improved) + * Smarter handling of symlinks in parent field +1. [](#bugfix) + * Fixed issue with windows paths in `parent` field [#1699](https://github.com/getgrav/grav-plugin-admin/issues/1699) + +# v1.10.0-beta.2 +## 06/21/2019 + +1. [](#improved) + * Moved Remodal in-house and added support for stackable modals [#1698](https://github.com/getgrav/grav-plugin-admin/issues/1698), [#1699](https://github.com/getgrav/grav-plugin-admin/issues/1699) +1. [](#bugfix) + * Fixed missing check for maximum allowed files in `files` field + +# v1.10.0-beta.1 +## 06/14/2019 + +1. [](#new) + * New Parent/Move field using Ajax for better performance + * Improvements to cache clearing when admin cache is enabled + * Require Grav v1.7 + * Use PSR-4 for plugin classes + * Added support for Twig 2.11 (compatible with Twig 1.40+) +1. [](#improved) + * Various admin performance improvements +1. [](#bugfix) + * Fixed admin caching issues + +# v1.9.19 +## 12/14/2020 + +1. [](#bugfix) + * Fixed `pages` field escaping issues, needs Grav update, too [#1990](https://github.com/getgrav/grav-plugin-admin/issues/1990) + * Fixed Plugins references in Themes details page. + * Fixed issue preventing purchase of Themes within Admin and redirecting instead. + * Fixed Page Picker not passing admin token + +# v1.9.18 +## 12/02/2020 + +1. [](#new) + * Never allow Admin pages to be rendered in ``, `'); + return true; + } + + return false; + } + + /** + * Generate HTML from item URL + * + * @access public + * @param Item $item + * @return bool + */ + public function generateHtmlFromUrl(Item $item) + { + if (preg_match('/youtube\.com\/watch\?v=(.*)/', $item->getUrl(), $matches)) { + $item->setContent(''); + return true; + } + + return false; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Logging/Logger.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Logging/Logger.php new file mode 100644 index 0000000..caec463 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Logging/Logger.php @@ -0,0 +1,114 @@ +format('Y-m-d H:i:s').'] '.$message; + } + } + + /** + * Get all logged messages. + * + * @static + * + * @return array + */ + public static function getMessages() + { + return self::$messages; + } + + /** + * Remove all logged messages. + * + * @static + */ + public static function deleteMessages() + { + self::$messages = array(); + } + + /** + * Set a different timezone. + * + * @static + * + * @see http://php.net/manual/en/timezones.php + * + * @param string $timezone Timezone + */ + public static function setTimeZone($timezone) + { + self::$timezone = $timezone ?: self::$timezone; + } + + /** + * Get all messages serialized into a string. + * + * @static + * + * @return string + */ + public static function toString() + { + return implode(PHP_EOL, self::$messages).PHP_EOL; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Atom.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Atom.php new file mode 100644 index 0000000..0496869 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Atom.php @@ -0,0 +1,395 @@ + 'http://www.w3.org/2005/Atom', + ); + + /** + * Get the path to the items XML tree. + * + * @param SimpleXMLElement $xml Feed xml + * @return SimpleXMLElement[] + */ + public function getItemsTree(SimpleXMLElement $xml) + { + return XmlParser::getXPathResult($xml, 'atom:entry', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'entry'); + } + + /** + * Find the feed url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedUrl(SimpleXMLElement $xml, Feed $feed) + { + $feed->setFeedUrl($this->getUrl($xml, 'self')); + } + + /** + * Find the site url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findSiteUrl(SimpleXMLElement $xml, Feed $feed) + { + $feed->setSiteUrl($this->getUrl($xml, 'alternate', true)); + } + + /** + * Find the feed description. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDescription(SimpleXMLElement $xml, Feed $feed) + { + $description = XmlParser::getXPathResult($xml, 'atom:subtitle', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'subtitle'); + + $feed->setDescription(XmlParser::getValue($description)); + } + + /** + * Find the feed logo url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLogo(SimpleXMLElement $xml, Feed $feed) + { + $logo = XmlParser::getXPathResult($xml, 'atom:logo', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'logo'); + + $feed->setLogo(XmlParser::getValue($logo)); + } + + /** + * Find the feed icon. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedIcon(SimpleXMLElement $xml, Feed $feed) + { + $icon = XmlParser::getXPathResult($xml, 'atom:icon', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'icon'); + + $feed->setIcon(XmlParser::getValue($icon)); + } + + /** + * Find the feed title. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedTitle(SimpleXMLElement $xml, Feed $feed) + { + $title = XmlParser::getXPathResult($xml, 'atom:title', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'title'); + + $feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl()); + } + + /** + * Find the feed language. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed) + { + $language = XmlParser::getXPathResult($xml, '*[not(self::atom:entry)]/@xml:lang', $this->namespaces) + ?: XmlParser::getXPathResult($xml, '@xml:lang'); + + $feed->setLanguage(XmlParser::getValue($language)); + } + + /** + * Find the feed id. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedId(SimpleXMLElement $xml, Feed $feed) + { + $id = XmlParser::getXPathResult($xml, 'atom:id', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'id'); + + $feed->setId(XmlParser::getValue($id)); + } + + /** + * Find the feed date. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDate(SimpleXMLElement $xml, Feed $feed) + { + $updated = XmlParser::getXPathResult($xml, 'atom:updated', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'updated'); + + $feed->setDate($this->getDateParser()->getDateTime(XmlParser::getValue($updated))); + } + + /** + * Find the item published date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemPublishedDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $date = XmlParser::getXPathResult($entry, 'atom:published', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'published'); + + $item->setPublishedDate(!empty($date) ? $this->getDateParser()->getDateTime((string) current($date)) : null); + } + + /** + * Find the item updated date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemUpdatedDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $date = XmlParser::getXPathResult($entry, 'atom:updated', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'updated'); + + $item->setUpdatedDate(!empty($date) ? $this->getDateParser()->getDateTime((string) current($date)) : null); + } + + /** + * Find the item title. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + */ + public function findItemTitle(SimpleXMLElement $entry, Item $item) + { + $title = XmlParser::getXPathResult($entry, 'atom:title', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'title'); + + $item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $item->getUrl()); + } + + /** + * Find the item author. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + $author = XmlParser::getXPathResult($entry, 'atom:author/atom:name', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'author/name') + ?: XmlParser::getXPathResult($xml, 'atom:author/atom:name', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'author/name'); + + $item->setAuthor(XmlParser::getValue($author)); + } + + /** + * Find the item author URL. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthorUrl(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + $authorUrl = XmlParser::getXPathResult($entry, 'atom:author/atom:uri', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'author/uri') + ?: XmlParser::getXPathResult($xml, 'atom:author/atom:uri', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'author/uri'); + + $item->setAuthorUrl(XmlParser::getValue($authorUrl)); + } + + /** + * Find the item content. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemContent(SimpleXMLElement $entry, Item $item) + { + $item->setContent($this->getContent($entry)); + } + + /** + * Find the item URL. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemUrl(SimpleXMLElement $entry, Item $item) + { + $item->setUrl($this->getUrl($entry, 'alternate', true)); + } + + /** + * Genereate the item id. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $id = XmlParser::getXPathResult($entry, 'atom:id', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'id'); + + if (!empty($id)) { + $item->setId($this->generateId(XmlParser::getValue($id))); + } else { + $item->setId($this->generateId( + $item->getTitle(), $item->getUrl(), $item->getContent() + )); + } + } + + /** + * Find the item enclosure. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $enclosure = $this->findLink($entry, 'enclosure'); + + if ($enclosure) { + $item->setEnclosureUrl(Url::resolve((string) $enclosure['href'], $feed->getSiteUrl())); + $item->setEnclosureType((string) $enclosure['type']); + } + } + + /** + * Find the item language. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $language = XmlParser::getXPathResult($entry, './/@xml:lang'); + $item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage()); + } + + /** + * Find the item categories. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param Feed $feed Feed object + */ + public function findItemCategories(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $categories = XmlParser::getXPathResult($entry, 'atom:category/@term', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'category/@term'); + $item->setCategoriesFromXml($categories); + } + + /** + * Get the URL from a link tag. + * + * @param SimpleXMLElement $xml XML tag + * @param string $rel Link relationship: alternate, enclosure, related, self, via + * @return string + */ + private function getUrl(SimpleXMLElement $xml, $rel, $fallback = false) + { + $link = $this->findLink($xml, $rel); + + if ($link) { + return (string) $link['href']; + } + + if ($fallback) { + $link = $this->findLink($xml, ''); + return $link ? (string) $link['href'] : ''; + } + + return ''; + } + + /** + * Get a link tag that match a relationship. + * + * @param SimpleXMLElement $xml XML tag + * @param string $rel Link relationship: alternate, enclosure, related, self, via + * @return SimpleXMLElement|null + */ + private function findLink(SimpleXMLElement $xml, $rel) + { + $links = XmlParser::getXPathResult($xml, 'atom:link', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'link'); + + foreach ($links as $link) { + if ($rel === (string) $link['rel']) { + return $link; + } + } + + return null; + } + + /** + * Get the entry content. + * + * @param SimpleXMLElement $entry XML Entry + * @return string + */ + private function getContent(SimpleXMLElement $entry) + { + $content = current( + XmlParser::getXPathResult($entry, 'atom:content', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'content') + ); + + if (!empty($content) && count($content->children())) { + $xml_string = ''; + + foreach ($content->children() as $child) { + $xml_string .= $child->asXML(); + } + + return $xml_string; + } elseif (trim((string) $content) !== '') { + return (string) $content; + } + + $summary = XmlParser::getXPathResult($entry, 'atom:summary', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'summary'); + + return (string) current($summary); + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/DateParser.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/DateParser.php new file mode 100644 index 0000000..0e5b80e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/DateParser.php @@ -0,0 +1,128 @@ + length ]. + * + * @var array + */ + public $formats = array( + DATE_ATOM => null, + DATE_RSS => null, + DATE_COOKIE => null, + DATE_ISO8601 => null, + DATE_RFC822 => null, + DATE_RFC850 => null, + DATE_RFC1036 => null, + DATE_RFC1123 => null, + DATE_RFC2822 => null, + DATE_RFC3339 => null, + 'l, d M Y H:i:s' => null, + 'D, d M Y H:i:s' => 25, + 'D, d M Y h:i:s' => 25, + 'D M d Y H:i:s' => 24, + 'j M Y H:i:s' => 20, + 'Y-m-d H:i:s' => 19, + 'Y-m-d\TH:i:s' => 19, + 'd/m/Y H:i:s' => 19, + 'D, d M Y' => 16, + 'Y-m-d' => 10, + 'd-m-Y' => 10, + 'm-d-Y' => 10, + 'd.m.Y' => 10, + 'm.d.Y' => 10, + 'd/m/Y' => 10, + 'm/d/Y' => 10, + ); + + /** + * Try to parse all date format for broken feeds. + * + * @param string $value Original date format + * + * @return DateTime + */ + public function getDateTime($value) + { + $value = trim($value); + + foreach ($this->formats as $format => $length) { + $truncated_value = $value; + if ($length !== null) { + $truncated_value = substr($truncated_value, 0, $length); + } + + $date = $this->getValidDate($format, $truncated_value); + if ($date !== false) { + return $date; + } + } + + return $this->getCurrentDateTime(); + } + + /** + * Get a valid date from a given format. + * + * @param string $format Date format + * @param string $value Original date value + * + * @return DateTime|bool + */ + public function getValidDate($format, $value) + { + $date = DateTime::createFromFormat($format, $value, $this->getTimeZone()); + + if ($date !== false) { + $errors = DateTime::getLastErrors(); + + if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) { + return $date; + } + } + + return false; + } + + /** + * Get the current datetime. + * + * @return DateTime + */ + public function getCurrentDateTime() + { + return new DateTime('now', $this->getTimeZone()); + } + + /** + * Get DateTimeZone instance + * + * @access public + * @return DateTimeZone + */ + public function getTimeZone() + { + return new DateTimeZone($this->config->getTimezone() ?: $this->timezone); + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Feed.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Feed.php new file mode 100644 index 0000000..a56e71c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Feed.php @@ -0,0 +1,315 @@ +$property.PHP_EOL; + } + + $output .= 'Feed::date = '.$this->date->format(DATE_RFC822).PHP_EOL; + $output .= 'Feed::isRTL() = '.($this->isRTL() ? 'true' : 'false').PHP_EOL; + $output .= 'Feed::items = '.count($this->items).' items'.PHP_EOL; + + foreach ($this->items as $item) { + $output .= '----'.PHP_EOL; + $output .= $item; + } + + return $output; + } + + /** + * Get title. + */ + public function getTitle() + { + return $this->title; + } + + /** + * Get description. + */ + public function getDescription() + { + return $this->description; + } + + /** + * Get the logo url. + */ + public function getLogo() + { + return $this->logo; + } + + /** + * Get the icon url. + */ + public function getIcon() + { + return $this->icon; + } + + /** + * Get feed url. + */ + public function getFeedUrl() + { + return $this->feedUrl; + } + + /** + * Get site url. + */ + public function getSiteUrl() + { + return $this->siteUrl; + } + + /** + * Get date. + */ + public function getDate() + { + return $this->date; + } + + /** + * Get language. + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Get id. + */ + public function getId() + { + return $this->id; + } + + /** + * Get feed items. + */ + public function getItems() + { + return $this->items; + } + + /** + * Return true if the feed is "Right to Left". + * + * @return bool + */ + public function isRTL() + { + return Parser::isLanguageRTL($this->language); + } + + /** + * Set feed items. + * + * @param Item[] $items + * @return Feed + */ + public function setItems(array $items) + { + $this->items = $items; + return $this; + } + + /** + * Set feed id. + * + * @param string $id + * @return Feed + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * Set feed title. + * + * @param string $title + * @return Feed + */ + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + /** + * Set feed description. + * + * @param string $description + * @return Feed + */ + public function setDescription($description) + { + $this->description = $description; + return $this; + } + + /** + * Set feed url. + * + * @param string $feedUrl + * @return Feed + */ + public function setFeedUrl($feedUrl) + { + $this->feedUrl = $feedUrl; + return $this; + } + + /** + * Set feed website url. + * + * @param string $siteUrl + * @return Feed + */ + public function setSiteUrl($siteUrl) + { + $this->siteUrl = $siteUrl; + return $this; + } + + /** + * Set feed date. + * + * @param \DateTime $date + * @return Feed + */ + public function setDate($date) + { + $this->date = $date; + return $this; + } + + /** + * Set feed language. + * + * @param string $language + * @return Feed + */ + public function setLanguage($language) + { + $this->language = $language; + return $this; + } + + /** + * Set feed logo. + * + * @param string $logo + * @return Feed + */ + public function setLogo($logo) + { + $this->logo = $logo; + return $this; + } + + /** + * Set feed icon. + * + * @param string $icon + * @return Feed + */ + public function setIcon($icon) + { + $this->icon = $icon; + return $this; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Item.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Item.php new file mode 100644 index 0000000..98214b8 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Item.php @@ -0,0 +1,562 @@ +namespaces); + } + + /** + * Get specific XML tag or attribute value. + * + * @param string $tag Tag name (examples: guid, media:content) + * @param string $attribute Tag attribute + * + * @return array|false Tag values or error + */ + public function getTag($tag, $attribute = '') + { + if ($attribute !== '') { + $attribute = '/@'.$attribute; + } + + $query = './/'.$tag.$attribute; + $elements = XmlParser::getXPathResult($this->xml, $query, $this->namespaces); + + if ($elements === false) { // xPath error + return false; + } + + return array_map(function ($element) { return (string) $element;}, $elements); + } + + /** + * Return item information. + * + * @return string + */ + public function __toString() + { + $output = ''; + + foreach (array('id', 'title', 'url', 'language', 'author', 'enclosureUrl', 'enclosureType') as $property) { + $output .= 'Item::'.$property.' = '.$this->$property.PHP_EOL; + } + + $publishedDate = $this->publishedDate != null ? $this->publishedDate->format(DATE_RFC822) : null; + $updatedDate = $this->updatedDate != null ? $this->updatedDate->format(DATE_RFC822) : null; + + $categoryString = $this->categories != null ? implode(',', $this->categories) : null; + + $output .= 'Item::date = '.$this->date->format(DATE_RFC822).PHP_EOL; + $output .= 'Item::publishedDate = '.$publishedDate.PHP_EOL; + $output .= 'Item::updatedDate = '.$updatedDate.PHP_EOL; + $output .= 'Item::isRTL() = '.($this->isRTL() ? 'true' : 'false').PHP_EOL; + $output .= 'Item::categories = ['.$categoryString.']'.PHP_EOL; + $output .= 'Item::content = '.strlen($this->content).' bytes'.PHP_EOL; + + return $output; + } + + /** + * Get title. + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Get URL + * + * @access public + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set URL + * + * @access public + * @param string $url + * @return Item + */ + public function setUrl($url) + { + $this->url = $url; + return $this; + } + + /** + * Get id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Get date. + * + * @return \DateTime + */ + public function getDate() + { + return $this->date; + } + + /** + * Get published date. + * + * @return \DateTime + */ + public function getPublishedDate() + { + return $this->publishedDate; + } + + /** + * Get updated date. + * + * @return \DateTime + */ + public function getUpdatedDate() + { + return $this->updatedDate; + } + + /** + * Get content. + * + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * Set content + * + * @access public + * @param string $value + * @return Item + */ + public function setContent($value) + { + $this->content = $value; + return $this; + } + + /** + * Get enclosure url. + * + * @return string + */ + public function getEnclosureUrl() + { + return $this->enclosureUrl; + } + + /** + * Get enclosure type. + * + * @return string + */ + public function getEnclosureType() + { + return $this->enclosureType; + } + + /** + * Get language. + * + * @return string + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Get categories. + * + * @return array + */ + public function getCategories() + { + return $this->categories; + } + + /** + * Get author. + * + * @return string + */ + public function getAuthor() + { + return $this->author; + } + + /** + * Get author URL. + * + * @return string + */ + public function getAuthorUrl() + { + return $this->authorUrl; + } + + /** + * Return true if the item is "Right to Left". + * + * @return bool + */ + public function isRTL() + { + return Parser::isLanguageRTL($this->language); + } + + /** + * Set item id. + * + * @param string $id + * @return Item + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * Set item title. + * + * @param string $title + * @return Item + */ + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + /** + * Set author. + * + * @param string $author + * @return Item + */ + public function setAuthor($author) + { + $this->author = $author; + return $this; + } + + /** + * Set author URL. + * + * @param string $authorUrl + * @return Item + */ + public function setAuthorUrl($authorUrl) + { + $this->authorUrl = $authorUrl; + return $this; + } + + /** + * Set item date. + * + * @param \DateTime $date + * @return Item + */ + public function setDate($date) + { + $this->date = $date; + return $this; + } + + /** + * Set item published date. + * + * @param \DateTime $publishedDate + * @return Item + */ + public function setPublishedDate($publishedDate) + { + $this->publishedDate = $publishedDate; + return $this; + } + + /** + * Set item updated date. + * + * @param \DateTime $updatedDate + * @return Item + */ + public function setUpdatedDate($updatedDate) + { + $this->updatedDate = $updatedDate; + return $this; + } + + /** + * Set enclosure url. + * + * @param string $enclosureUrl + * @return Item + */ + public function setEnclosureUrl($enclosureUrl) + { + $this->enclosureUrl = $enclosureUrl; + return $this; + } + + /** + * Set enclosure type. + * + * @param string $enclosureType + * @return Item + */ + public function setEnclosureType($enclosureType) + { + $this->enclosureType = $enclosureType; + return $this; + } + + /** + * Set item language. + * + * @param string $language + * @return Item + */ + public function setLanguage($language) + { + $this->language = $language; + return $this; + } + + /** + * Set item categories. + * + * @param array $categories + * @return Item + */ + public function setCategories($categories) + { + $this->categories = $categories; + return $this; + } + + /** + * Set item categories from xml. + * + * @param |SimpleXMLElement[] $categories + * @return Item + */ + public function setCategoriesFromXml($categories) + { + if ($categories !== false) { + $this->setCategories( + array_map( + function ($element) { + return trim((string) $element); + }, + $categories + ) + ); + } + + return $this; + } + + /** + * Set raw XML. + * + * @param \SimpleXMLElement $xml + * @return Item + */ + public function setXml($xml) + { + $this->xml = $xml; + return $this; + } + + /** + * Get raw XML. + * + * @return \SimpleXMLElement + */ + public function getXml() + { + return $this->xml; + } + + /** + * Set XML namespaces. + * + * @param array $namespaces + * @return Item + */ + public function setNamespaces($namespaces) + { + $this->namespaces = $namespaces; + return $this; + } + + /** + * Get XML namespaces. + * + * @return array + */ + public function getNamespaces() + { + return $this->namespaces; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/MalformedXmlException.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/MalformedXmlException.php new file mode 100644 index 0000000..efaf0ff --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/MalformedXmlException.php @@ -0,0 +1,13 @@ +fallback_url = $fallback_url; + $xml_encoding = XmlParser::getEncodingFromXmlTag($content); + + // Strip XML tag to avoid multiple encoding/decoding in the next XML processing + $this->content = Filter::stripXmlTag($content); + + // Encode everything in UTF-8 + Logger::setMessage(get_called_class().': HTTP Encoding "'.$http_encoding.'" ; XML Encoding "'.$xml_encoding.'"'); + $this->content = Encoding::convert($this->content, $xml_encoding ?: $http_encoding); + + $this->itemPostProcessor = new ItemPostProcessor($this->config); + $this->itemPostProcessor->register(new ContentGeneratorProcessor($this->config)); + $this->itemPostProcessor->register(new ContentFilterProcessor($this->config)); + } + + /** + * Parse the document. + * @return Feed + * @throws MalformedXmlException + */ + public function execute() + { + Logger::setMessage(get_called_class().': begin parsing'); + + $xml = XmlParser::getSimpleXml($this->content); + + if ($xml === false) { + Logger::setMessage(get_called_class().': Applying XML workarounds'); + $this->content = Filter::normalizeData($this->content); + $xml = XmlParser::getSimpleXml($this->content); + + if ($xml === false) { + Logger::setMessage(get_called_class().': XML parsing error'); + Logger::setMessage(XmlParser::getErrors()); + throw new MalformedXmlException('XML parsing error'); + } + } + + $this->used_namespaces = $xml->getNamespaces(true); + $xml = $this->registerSupportedNamespaces($xml); + + $feed = new Feed(); + + $this->findFeedUrl($xml, $feed); + $this->checkFeedUrl($feed); + + $this->findSiteUrl($xml, $feed); + $this->checkSiteUrl($feed); + + $this->findFeedTitle($xml, $feed); + $this->findFeedDescription($xml, $feed); + $this->findFeedLanguage($xml, $feed); + $this->findFeedId($xml, $feed); + $this->findFeedDate($xml, $feed); + $this->findFeedLogo($xml, $feed); + $this->findFeedIcon($xml, $feed); + + foreach ($this->getItemsTree($xml) as $entry) { + $entry = $this->registerSupportedNamespaces($entry); + + $item = new Item(); + $item->xml = $entry; + $item->namespaces = $this->used_namespaces; + + $this->findItemAuthor($xml, $entry, $item); + $this->findItemAuthorUrl($xml, $entry, $item); + + $this->findItemUrl($entry, $item); + $this->checkItemUrl($feed, $item); + + $this->findItemTitle($entry, $item); + $this->findItemContent($entry, $item); + + // Id generation can use the item url/title/content (order is important) + $this->findItemId($entry, $item, $feed); + $this->findItemDate($entry, $item, $feed); + $this->findItemEnclosure($entry, $item, $feed); + $this->findItemLanguage($entry, $item, $feed); + $this->findItemCategories($entry, $item, $feed); + + $this->itemPostProcessor->execute($feed, $item); + $feed->items[] = $item; + } + + Logger::setMessage(get_called_class().PHP_EOL.$feed); + + return $feed; + } + + /** + * Check if the feed url is correct. + * + * @param Feed $feed Feed object + */ + public function checkFeedUrl(Feed $feed) + { + if ($feed->getFeedUrl() === '') { + $feed->feedUrl = $this->fallback_url; + } else { + $feed->feedUrl = Url::resolve($feed->getFeedUrl(), $this->fallback_url); + } + } + + /** + * Check if the site url is correct. + * + * @param Feed $feed Feed object + */ + public function checkSiteUrl(Feed $feed) + { + if ($feed->getSiteUrl() === '') { + $feed->siteUrl = Url::base($feed->getFeedUrl()); + } else { + $feed->siteUrl = Url::resolve($feed->getSiteUrl(), $this->fallback_url); + } + } + + /** + * Check if the item url is correct. + * + * @param Feed $feed Feed object + * @param Item $item Item object + */ + public function checkItemUrl(Feed $feed, Item $item) + { + $item->url = Url::resolve($item->getUrl(), $feed->getSiteUrl()); + } + + /** + * Find the item date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $this->findItemPublishedDate($entry, $item, $feed); + $this->findItemUpdatedDate($entry, $item, $feed); + + if ($item->getPublishedDate() === null) { + // Use the updated date if available, otherwise use the feed date + $item->setPublishedDate($item->getUpdatedDate() ?: $feed->getDate()); + } + + if ($item->getUpdatedDate() === null) { + // Use the published date as fallback + $item->setUpdatedDate($item->getPublishedDate()); + } + + // Use the most recent of published and updated dates + $item->setDate(max($item->getPublishedDate(), $item->getUpdatedDate())); + } + + /** + * Get Item Post Processor instance + * + * @access public + * @return ItemPostProcessor + */ + public function getItemPostProcessor() + { + return $this->itemPostProcessor; + } + + /** + * Get DateParser instance + * + * @access public + * @return DateParser + */ + public function getDateParser() + { + if ($this->dateParser === null) { + $this->dateParser = new DateParser($this->config); + } + + return $this->dateParser; + } + + /** + * Generate a unique id for an entry (hash all arguments). + * + * @return string + */ + public function generateId() + { + return hash($this->hash_algo, implode(func_get_args())); + } + + /** + * Return true if the given language is "Right to Left". + * + * @static + * @param string $language Language: fr-FR, en-US + * @return bool + */ + public static function isLanguageRTL($language) + { + $language = strtolower($language); + + $rtl_languages = array( + 'ar', // Arabic (ar-**) + 'fa', // Farsi (fa-**) + 'ur', // Urdu (ur-**) + 'ps', // Pashtu (ps-**) + 'syr', // Syriac (syr-**) + 'dv', // Divehi (dv-**) + 'he', // Hebrew (he-**) + 'yi', // Yiddish (yi-**) + ); + + foreach ($rtl_languages as $prefix) { + if (strpos($language, $prefix) === 0) { + return true; + } + } + + return false; + } + + /** + * Set Hash algorithm used for id generation. + * + * @param string $algo Algorithm name + * @return \PicoFeed\Parser\Parser + */ + public function setHashAlgo($algo) + { + $this->hash_algo = $algo ?: $this->hash_algo; + return $this; + } + + /** + * Set config object. + * + * @param \PicoFeed\Config\Config $config Config instance + * @return \PicoFeed\Parser\Parser + */ + public function setConfig($config) + { + $this->config = $config; + $this->itemPostProcessor->setConfig($config); + return $this; + } + + /** + * Enable the content grabber. + * + * @return \PicoFeed\Parser\Parser + */ + public function disableContentFiltering() + { + $this->itemPostProcessor->unregister('PicoFeed\Processor\ContentFilterProcessor'); + return $this; + } + + /** + * Enable the content grabber. + * + * @param bool $needsRuleFile true if only pages with rule files should be + * scraped + * @param null|\Closure $scraperCallback Callback function that gets called for each + * scraper execution + * @return \PicoFeed\Parser\Parser + */ + public function enableContentGrabber($needsRuleFile = false, $scraperCallback = null) + { + $processor = new ScraperProcessor($this->config); + + if ($needsRuleFile) { + $processor->getScraper()->disableCandidateParser(); + } + + if ($scraperCallback !== null) { + $processor->setExecutionCallback($scraperCallback); + } + + $this->itemPostProcessor->register($processor); + return $this; + } + + /** + * Set ignored URLs for the content grabber. + * + * @param array $urls URLs + * @return \PicoFeed\Parser\Parser + */ + public function setGrabberIgnoreUrls(array $urls) + { + $this->itemPostProcessor->getProcessor('PicoFeed\Processor\ScraperProcessor')->ignoreUrls($urls); + return $this; + } + + /** + * Register all supported namespaces to be used within an xpath query. + * + * @param SimpleXMLElement $xml Feed xml + * @return SimpleXMLElement + */ + public function registerSupportedNamespaces(SimpleXMLElement $xml) + { + foreach ($this->namespaces as $prefix => $ns) { + $xml->registerXPathNamespace($prefix, $ns); + } + + return $xml; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/ParserException.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/ParserException.php new file mode 100644 index 0000000..b5fbb69 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/ParserException.php @@ -0,0 +1,15 @@ + 'http://purl.org/rss/1.0/', + 'dc' => 'http://purl.org/dc/elements/1.1/', + 'content' => 'http://purl.org/rss/1.0/modules/content/', + 'feedburner' => 'http://rssnamespace.org/feedburner/ext/1.0', + ); + + /** + * Get the path to the items XML tree. + * + * @param SimpleXMLElement $xml Feed xml + * @return SimpleXMLElement[] + */ + public function getItemsTree(SimpleXMLElement $xml) + { + return XmlParser::getXPathResult($xml, 'rss:item', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'item') + ?: $xml->item; + } + + /** + * Find the feed url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedUrl(SimpleXMLElement $xml, Feed $feed) + { + $feed->setFeedUrl(''); + } + + /** + * Find the site url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findSiteUrl(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'rss:channel/rss:link', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/link') + ?: $xml->channel->link; + + $feed->setSiteUrl(XmlParser::getValue($value)); + } + + /** + * Find the feed description. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDescription(SimpleXMLElement $xml, Feed $feed) + { + $description = XmlParser::getXPathResult($xml, 'rss:channel/rss:description', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/description') + ?: $xml->channel->description; + + $feed->setDescription(XmlParser::getValue($description)); + } + + /** + * Find the feed logo url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLogo(SimpleXMLElement $xml, Feed $feed) + { + $logo = XmlParser::getXPathResult($xml, 'rss:image/rss:url', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'image/url'); + + $feed->setLogo(XmlParser::getValue($logo)); + } + + /** + * Find the feed icon. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedIcon(SimpleXMLElement $xml, Feed $feed) + { + $feed->setIcon(''); + } + + /** + * Find the feed title. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedTitle(SimpleXMLElement $xml, Feed $feed) + { + $title = XmlParser::getXPathResult($xml, 'rss:channel/rss:title', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/title') + ?: $xml->channel->title; + + $feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl()); + } + + /** + * Find the feed language. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed) + { + $language = XmlParser::getXPathResult($xml, 'rss:channel/dc:language', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/dc:language', $this->namespaces); + + $feed->setLanguage(XmlParser::getValue($language)); + } + + /** + * Find the feed id. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedId(SimpleXMLElement $xml, Feed $feed) + { + $feed->setId($feed->getFeedUrl() ?: $feed->getSiteUrl()); + } + + /** + * Find the feed date. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDate(SimpleXMLElement $xml, Feed $feed) + { + $date = XmlParser::getXPathResult($xml, 'rss:channel/dc:date', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/dc:date', $this->namespaces); + + $feed->setDate($this->getDateParser()->getDateTime(XmlParser::getValue($date))); + } + + /** + * Find the item published date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemPublishedDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $date = XmlParser::getXPathResult($entry, 'dc:date', $this->namespaces); + + $item->setPublishedDate(!empty($date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($date)) : null); + } + + /** + * Find the item updated date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemUpdatedDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + if ($item->publishedDate === null) { + $this->findItemPublishedDate($entry, $item, $feed); + } + $item->setUpdatedDate($item->getPublishedDate()); // No updated date in RSS 1.0 specifications + } + + /** + * Find the item title. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemTitle(SimpleXMLElement $entry, Item $item) + { + $title = XmlParser::getXPathResult($entry, 'rss:title', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'title') + ?: $entry->title; + + $item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $item->getUrl()); + } + + /** + * Find the item author. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + $author = XmlParser::getXPathResult($entry, 'dc:creator', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'rss:channel/dc:creator', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/dc:creator', $this->namespaces); + + $item->setAuthor(XmlParser::getValue($author)); + } + + /** + * Find the item author URL. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthorUrl(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + // There appears to be no support for author URL in the dc: terms + $item->setAuthorUrl(''); + } + + /** + * Find the item content. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemContent(SimpleXMLElement $entry, Item $item) + { + $content = XmlParser::getXPathResult($entry, 'content:encoded', $this->namespaces); + + if (XmlParser::getValue($content) === '') { + $content = XmlParser::getXPathResult($entry, 'rss:description', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'description') + ?: $entry->description; + } + + $item->setContent(XmlParser::getValue($content)); + } + + /** + * Find the item URL. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemUrl(SimpleXMLElement $entry, Item $item) + { + $link = XmlParser::getXPathResult($entry, 'feedburner:origLink', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'rss:link', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'link') + ?: $entry->link; + + $item->setUrl(XmlParser::getValue($link)); + } + + /** + * Genereate the item id. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $item->setId($this->generateId( + $item->getTitle(), $item->getUrl(), $item->getContent() + )); + } + + /** + * Find the item enclosure. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed) + { + } + + /** + * Find the item language. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $language = XmlParser::getXPathResult($entry, 'dc:language', $this->namespaces); + + $item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage()); + } + + /** + * Find the item categories. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param Feed $feed Feed object + */ + public function findItemCategories(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $categories = XmlParser::getXPathResult($entry, 'dc:subject', $this->namespaces); + $item->setCategoriesFromXml($categories); + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss20.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss20.php new file mode 100644 index 0000000..da9c0d5 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss20.php @@ -0,0 +1,330 @@ + 'http://purl.org/dc/elements/1.1/', + 'content' => 'http://purl.org/rss/1.0/modules/content/', + 'feedburner' => 'http://rssnamespace.org/feedburner/ext/1.0', + 'atom' => 'http://www.w3.org/2005/Atom', + ); + + /** + * Get the path to the items XML tree. + * + * @param SimpleXMLElement $xml Feed xml + * @return SimpleXMLElement[] + */ + public function getItemsTree(SimpleXMLElement $xml) + { + return XmlParser::getXPathResult($xml, 'channel/item'); + } + + /** + * Find the feed url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedUrl(SimpleXMLElement $xml, Feed $feed) + { + $feed->setFeedUrl(''); + } + + /** + * Find the site url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findSiteUrl(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'channel/link'); + $feed->setSiteUrl(XmlParser::getValue($value)); + } + + /** + * Find the feed description. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDescription(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'channel/description'); + $feed->setDescription(XmlParser::getValue($value)); + } + + /** + * Find the feed logo url. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLogo(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'channel/image/url'); + $feed->setLogo(XmlParser::getValue($value)); + } + + /** + * Find the feed icon. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedIcon(SimpleXMLElement $xml, Feed $feed) + { + $feed->setIcon(''); + } + + /** + * Find the feed title. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedTitle(SimpleXMLElement $xml, Feed $feed) + { + $title = XmlParser::getXPathResult($xml, 'channel/title'); + $feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl()); + } + + /** + * Find the feed language. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed) + { + $value = XmlParser::getXPathResult($xml, 'channel/language'); + $feed->setLanguage(XmlParser::getValue($value)); + } + + /** + * Find the feed id. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedId(SimpleXMLElement $xml, Feed $feed) + { + $feed->setId($feed->getFeedUrl() ?: $feed->getSiteUrl()); + } + + /** + * Find the feed date. + * + * @param SimpleXMLElement $xml Feed xml + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findFeedDate(SimpleXMLElement $xml, Feed $feed) + { + $publish_date = XmlParser::getXPathResult($xml, 'channel/pubDate'); + $update_date = XmlParser::getXPathResult($xml, 'channel/lastBuildDate'); + + $published = !empty($publish_date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($publish_date)) : null; + $updated = !empty($update_date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($update_date)) : null; + + if ($published === null && $updated === null) { + $feed->setDate($this->getDateParser()->getCurrentDateTime()); // We use the current date if there is no date for the feed + } elseif ($published !== null && $updated !== null) { + $feed->setDate(max($published, $updated)); // We use the most recent date between published and updated + } else { + $feed->setDate($updated ?: $published); + } + } + + /** + * Find the item published date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemPublishedDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $date = XmlParser::getXPathResult($entry, 'pubDate'); + + $item->setPublishedDate(!empty($date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($date)) : null); + } + + /** + * Find the item updated date. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemUpdatedDate(SimpleXMLElement $entry, Item $item, Feed $feed) + { + if ($item->publishedDate === null) { + $this->findItemPublishedDate($entry, $item, $feed); + } + $item->setUpdatedDate($item->getPublishedDate()); // No updated date in RSS 2.0 specifications + } + + /** + * Find the item title. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemTitle(SimpleXMLElement $entry, Item $item) + { + $value = XmlParser::getXPathResult($entry, 'title'); + $item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($value)) ?: $item->getUrl()); + } + + /** + * Find the item author. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + $value = XmlParser::getXPathResult($entry, 'dc:creator', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'author') + ?: XmlParser::getXPathResult($xml, 'channel/dc:creator', $this->namespaces) + ?: XmlParser::getXPathResult($xml, 'channel/managingEditor'); + + $item->setAuthor(XmlParser::getValue($value)); + } + + /** + * Find the item author URL. + * + * @param SimpleXMLElement $xml Feed + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemAuthorUrl(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) + { + // There appears to be no support for author URL in the dc: terms or author element + $item->setAuthorUrl(''); + } + + /** + * Find the item content. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemContent(SimpleXMLElement $entry, Item $item) + { + $content = XmlParser::getXPathResult($entry, 'content:encoded', $this->namespaces); + + if (XmlParser::getValue($content) === '') { + $content = XmlParser::getXPathResult($entry, 'description'); + } + + $item->setContent(XmlParser::getValue($content)); + } + + /** + * Find the item URL. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + */ + public function findItemUrl(SimpleXMLElement $entry, Item $item) + { + $link = XmlParser::getXPathResult($entry, 'feedburner:origLink', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'link') + ?: XmlParser::getXPathResult($entry, 'atom:link/@href', $this->namespaces); + + if (!empty($link)) { + $item->setUrl(XmlParser::getValue($link)); + } else { + $link = XmlParser::getXPathResult($entry, 'guid'); + $link = XmlParser::getValue($link); + + if (filter_var($link, FILTER_VALIDATE_URL) !== false) { + $item->setUrl($link); + } + } + } + + /** + * Genereate the item id. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $id = XmlParser::getValue(XmlParser::getXPathResult($entry, 'guid')); + + if ($id) { + $item->setId($this->generateId($id)); + } else { + $item->setId($this->generateId( + $item->getTitle(), $item->getUrl(), $item->getContent() + )); + } + } + + /** + * Find the item enclosure. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed) + { + if (isset($entry->enclosure)) { + $type = XmlParser::getXPathResult($entry, 'enclosure/@type'); + $url = XmlParser::getXPathResult($entry, 'feedburner:origEnclosureLink', $this->namespaces) + ?: XmlParser::getXPathResult($entry, 'enclosure/@url'); + + $item->setEnclosureUrl(Url::resolve(XmlParser::getValue($url), $feed->getSiteUrl())); + $item->setEnclosureType(XmlParser::getValue($type)); + } + } + + /** + * Find the item language. + * + * @param SimpleXMLElement $entry Feed item + * @param \PicoFeed\Parser\Item $item Item object + * @param \PicoFeed\Parser\Feed $feed Feed object + */ + public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $language = XmlParser::getXPathResult($entry, 'dc:language', $this->namespaces); + $item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage()); + } + + /** + * Find the item categories. + * + * @param SimpleXMLElement $entry Feed item + * @param Item $item Item object + * @param Feed $feed Feed object + */ + public function findItemCategories(SimpleXMLElement $entry, Item $item, Feed $feed) + { + $categories = XmlParser::getXPathResult($entry, 'category'); + $item->setCategoriesFromXml($categories); + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss91.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss91.php new file mode 100644 index 0000000..058fca1 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Parser/Rss91.php @@ -0,0 +1,13 @@ +childNodes->length === 0) { + return false; + } + + return $dom; + } + + /** + * Small wrapper around Laminas Xml to turn their exceptions into PicoFeed exceptions + * + * @static + * @access private + * @param string $input + * @param DOMDocument $dom + * @throws XmlEntityException + * @return SimpleXMLElement|DomDocument|boolean + */ + private static function scan($input, $dom = null) + { + try { + return Security::scan($input, $dom); + } catch(RuntimeException $e) { + throw new XmlEntityException($e->getMessage()); + } + } + + /** + * Load HTML document by using a DomDocument instance or return false on failure. + * + * @static + * @access public + * @param string $input XML content + * @return DOMDocument + */ + public static function getHtmlDocument($input) + { + $dom = new DomDocument(); + + if (empty($input)) { + return $dom; + } + + libxml_use_internal_errors(true); + + if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + $dom->loadHTML($input, LIBXML_NONET); + } else { + $dom->loadHTML($input); + } + + self::$errors = []; + foreach (libxml_get_errors() as $error) { + self::$errors[] = sprintf('XML error: %s (Line: %d - Column: %d - Code: %d)', + $error->message, + $error->line, + $error->column, + $error->code + ); + } + + libxml_use_internal_errors(false); + + return $dom; + } + + /** + * Convert a HTML document to XML. + * + * @static + * @access public + * @param string $html HTML document + * @return string + */ + public static function htmlToXml($html) + { + $dom = self::getHtmlDocument(''.$html); + return $dom->saveXML($dom->getElementsByTagName('body')->item(0)); + } + + /** + * Get XML parser errors. + * + * @static + * @access public + * @return string + */ + public static function getErrors() + { + return implode(', ', self::$errors); + } + + /** + * Get the encoding from a xml tag. + * + * @static + * @access public + * @param string $data Input data + * @return string + */ + public static function getEncodingFromXmlTag($data) + { + $encoding = ''; + + if (strpos($data, '')); + $data = str_replace("'", '"', $data); + + $p1 = strpos($data, 'encoding='); + $p2 = strpos($data, '"', $p1 + 10); + + if ($p1 !== false && $p2 !== false) { + $encoding = substr($data, $p1 + 10, $p2 - $p1 - 10); + $encoding = strtolower($encoding); + } + } + + return $encoding; + } + + /** + * Get the charset from a meta tag. + * + * @static + * @access public + * @param string $data Input data + * @return string + */ + public static function getEncodingFromMetaTag($data) + { + $encoding = ''; + + if (preg_match('/;]+)/i', $data, $match) === 1) { + $encoding = strtolower($match[1]); + } + + return $encoding; + } + + /** + * Rewrite XPath query to use namespace-uri and local-name derived from prefix. + * + * @static + * @access public + * @param string $query XPath query + * @param array $ns Prefix to namespace URI mapping + * @return string + */ + public static function replaceXPathPrefixWithNamespaceURI($query, array $ns) + { + return preg_replace_callback('/([A-Z0-9]+):([A-Z0-9]+)/iu', function ($matches) use ($ns) { + // don't try to map the special prefix XML + if (strtolower($matches[1]) === 'xml') { + return $matches[0]; + } + + return '*[namespace-uri()="'.$ns[$matches[1]].'" and local-name()="'.$matches[2].'"]'; + }, + $query); + } + + /** + * Get the result elements of a XPath query. + * + * @static + * @access public + * @param SimpleXMLElement $xml XML element + * @param string $query XPath query + * @param array $ns Prefix to namespace URI mapping + * @return SimpleXMLElement[] + */ + public static function getXPathResult(SimpleXMLElement $xml, $query, array $ns = array()) + { + if (!empty($ns)) { + $query = static::replaceXPathPrefixWithNamespaceURI($query, $ns); + } + + return $xml->xpath($query); + } + + /** + * Get the first Xpath result or SimpleXMLElement value + * + * @static + * @access public + * @param mixed $value + * @return string + */ + public static function getValue($value) + { + $result = ''; + + if (is_array($value) && count($value) > 0) { + $result = (string) $value[0]; + } elseif (is_a($value, 'SimpleXMLElement')) { + return $result = (string) $value; + } + + return trim($result); + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/PicoFeedException.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/PicoFeedException.php new file mode 100644 index 0000000..2de9e4b --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/PicoFeedException.php @@ -0,0 +1,14 @@ +config->getContentFiltering(true)) { + $filter = Filter::html($item->getContent(), $feed->getSiteUrl()); + $filter->setConfig($this->config); + $item->setContent($filter->execute()); + } else { + Logger::setMessage(get_called_class().': Content filtering disabled'); + } + + return false; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ContentGeneratorProcessor.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ContentGeneratorProcessor.php new file mode 100644 index 0000000..49adf9c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ContentGeneratorProcessor.php @@ -0,0 +1,49 @@ +generators as $generator) { + $className = '\PicoFeed\Generator\\'.ucfirst($generator).'ContentGenerator'; + $object = new $className($this->config); + + if ($object->execute($item)) { + return true; + } + } + + return false; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemPostProcessor.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemPostProcessor.php new file mode 100644 index 0000000..7b092b5 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemPostProcessor.php @@ -0,0 +1,106 @@ +processors as $processor) { + if ($processor->execute($feed, $item)) { + return true; + } + } + + return false; + } + + /** + * Register a new Item post-processor + * + * @access public + * @param ItemProcessorInterface $processor + * @return ItemPostProcessor + */ + public function register(ItemProcessorInterface $processor) + { + $this->processors[get_class($processor)] = $processor; + return $this; + } + + /** + * Remove Processor instance + * + * @access public + * @param string $class + * @return ItemPostProcessor + */ + public function unregister($class) + { + if (isset($this->processors[$class])) { + unset($this->processors[$class]); + } + + return $this; + } + + /** + * Checks whether a specific processor is registered or not + * + * @access public + * @param string $class + * @return bool + */ + public function hasProcessor($class) + { + return isset($this->processors[$class]); + } + + /** + * Get Processor instance + * + * @access public + * @param string $class + * @return ItemProcessorInterface|null + */ + public function getProcessor($class) + { + return isset($this->processors[$class]) ? $this->processors[$class] : null; + } + + public function setConfig(Config $config) + { + foreach ($this->processors as $processor) { + $processor->setConfig($config); + } + + return false; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemProcessorInterface.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemProcessorInterface.php new file mode 100644 index 0000000..5d53226 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Processor/ItemProcessorInterface.php @@ -0,0 +1,25 @@ +executionCallback = $executionCallback; + return $this; + } + + /** + * Execute Item Processor + * + * @access public + * @param Feed $feed + * @param Item $item + * @return bool + */ + public function execute(Feed $feed, Item $item) + { + if (!in_array($item->getUrl(), $this->ignoredUrls)) { + $scraper = $this->getScraper(); + $scraper->setUrl($item->getUrl()); + $scraper->execute(); + + if ($this->executionCallback && is_callable($this->executionCallback)) { + call_user_func($this->executionCallback, $feed, $item, $scraper); + } + + if ($scraper->hasRelevantContent()) { + $item->setContent($scraper->getFilteredContent()); + } + } + + return false; + } + + /** + * Ignore list of URLs + * + * @access public + * @param array $urls + * @return $this + */ + public function ignoreUrls(array $urls) + { + $this->ignoredUrls = $urls; + return $this; + } + + /** + * Returns Scraper instance + * + * @access public + * @return Scraper + */ + public function getScraper() + { + if ($this->scraper === null) { + $this->scraper = new Scraper($this->config); + } + + return $this->scraper; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Favicon.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Favicon.php new file mode 100644 index 0000000..d4ca07d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Favicon.php @@ -0,0 +1,186 @@ +content; + } + + /** + * Get the icon file type (available only after the download). + * + * @return string + */ + public function getType() + { + foreach ($this->types as $type) { + if (strpos($this->content_type, $type) === 0) { + return $type; + } + } + + return 'image/x-icon'; + } + + /** + * Get data URI (http://en.wikipedia.org/wiki/Data_URI_scheme). + * + * @return string + */ + public function getDataUri() + { + if (empty($this->content)) { + return ''; + } + + return sprintf( + 'data:%s;base64,%s', + $this->getType(), + base64_encode($this->content) + ); + } + + /** + * Download and check if a resource exists. + * + * @param string $url URL + * @return \PicoFeed\Client\Client Client instance + */ + public function download($url) + { + $client = Client::getInstance(); + $client->setConfig($this->config); + + Logger::setMessage(get_called_class().' Download => '.$url); + + try { + $client->execute($url); + } catch (ClientException $e) { + Logger::setMessage(get_called_class().' Download Failed => '.$e->getMessage()); + } + + return $client; + } + + /** + * Check if a remote file exists. + * + * @param string $url URL + * @return bool + */ + public function exists($url) + { + return $this->download($url)->getContent() !== ''; + } + + /** + * Get the icon link for a website. + * + * @param string $website_link URL + * @param string $favicon_link optional URL + * @return string + */ + public function find($website_link, $favicon_link = '') + { + $website = new Url($website_link); + + if ($favicon_link !== '') { + $icons = array($favicon_link); + } else { + $icons = $this->extract($this->download($website->getBaseUrl('/'))->getContent()); + $icons[] = $website->getBaseUrl('/favicon.ico'); + } + + foreach ($icons as $icon_link) { + $icon_link = Url::resolve($icon_link, $website); + $resource = $this->download($icon_link); + $this->content = $resource->getContent(); + $this->content_type = $resource->getContentType(); + + if ($this->content !== '') { + return $icon_link; + } elseif ($favicon_link !== '') { + return $this->find($website_link); + } + } + + return ''; + } + + /** + * Extract the icon links from the HTML. + * + * @param string $html HTML + * @return array + */ + public function extract($html) + { + $icons = array(); + + if (empty($html)) { + return $icons; + } + + $dom = XmlParser::getHtmlDocument($html); + + $xpath = new DOMXpath($dom); + $elements = $xpath->query('//link[@rel="icon" or @rel="shortcut icon" or @rel="Shortcut Icon" or @rel="icon shortcut"]'); + + for ($i = 0; $i < $elements->length; ++$i) { + $icons[] = $elements->item($i)->getAttribute('href'); + } + + return $icons; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Reader.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Reader.php new file mode 100644 index 0000000..596b02d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/Reader.php @@ -0,0 +1,189 @@ + '//feed', + 'Rss20' => '//rss[@version="2.0"]', + 'Rss92' => '//rss[@version="0.92"]', + 'Rss91' => '//rss[@version="0.91"]', + 'Rss10' => '//rdf', + ); + + /** + * Download a feed (no discovery). + * + * @param string $url Feed url + * @param string $last_modified Last modified HTTP header + * @param string $etag Etag HTTP header + * @param string $username HTTP basic auth username + * @param string $password HTTP basic auth password + * + * @return \PicoFeed\Client\Client + */ + public function download($url, $last_modified = '', $etag = '', $username = '', $password = '') + { + $url = $this->prependScheme($url); + + return Client::getInstance() + ->setConfig($this->config) + ->setLastModified($last_modified) + ->setEtag($etag) + ->setUsername($username) + ->setPassword($password) + ->execute($url); + } + + /** + * Discover and download a feed. + * + * @param string $url Feed or website url + * @param string $last_modified Last modified HTTP header + * @param string $etag Etag HTTP header + * @param string $username HTTP basic auth username + * @param string $password HTTP basic auth password + * @return Client + * @throws SubscriptionNotFoundException + */ + public function discover($url, $last_modified = '', $etag = '', $username = '', $password = '') + { + $client = $this->download($url, $last_modified, $etag, $username, $password); + + // It's already a feed or the feed was not modified + if (!$client->isModified() || $this->detectFormat($client->getContent())) { + return $client; + } + + // Try to find a subscription + $links = $this->find($client->getUrl(), $client->getContent()); + + if (empty($links)) { + throw new SubscriptionNotFoundException('Unable to find a subscription'); + } + + return $this->download($links[0], $last_modified, $etag, $username, $password); + } + + /** + * Find feed urls inside a HTML document. + * + * @param string $url Website url + * @param string $html HTML content + * + * @return array List of feed links + */ + public function find($url, $html) + { + Logger::setMessage(get_called_class().': Try to discover subscriptions'); + + $dom = XmlParser::getHtmlDocument($html); + $xpath = new DOMXPath($dom); + $links = array(); + + $queries = array( + '//link[@type="application/rss+xml"]', + '//link[@type="application/atom+xml"]', + ); + + foreach ($queries as $query) { + $nodes = $xpath->query($query); + + foreach ($nodes as $node) { + $link = $node->getAttribute('href'); + + if (!empty($link)) { + $feedUrl = new Url($link); + $siteUrl = new Url($url); + + $links[] = $feedUrl->getAbsoluteUrl($feedUrl->isRelativeUrl() ? $siteUrl->getBaseUrl() : ''); + } + } + } + + Logger::setMessage(get_called_class().': '.implode(', ', $links)); + + return $links; + } + + /** + * Get a parser instance. + * + * @param string $url Site url + * @param string $content Feed content + * @param string $encoding HTTP encoding + * @return \PicoFeed\Parser\Parser + * @throws UnsupportedFeedFormatException + */ + public function getParser($url, $content, $encoding) + { + $format = $this->detectFormat($content); + + if (empty($format)) { + throw new UnsupportedFeedFormatException('Unable to detect feed format'); + } + + $className = '\PicoFeed\Parser\\'.$format; + + $parser = new $className($content, $encoding, $url); + $parser->setHashAlgo($this->config->getParserHashAlgo()); + $parser->setConfig($this->config); + + return $parser; + } + + /** + * Detect the feed format. + * + * @param string $content Feed content + * @return string + */ + public function detectFormat($content) + { + $dom = XmlParser::getHtmlDocument($content); + $xpath = new DOMXPath($dom); + + foreach ($this->formats as $parser_name => $query) { + $nodes = $xpath->query($query); + + if ($nodes->length === 1) { + return $parser_name; + } + } + + return ''; + } + + /** + * Add the prefix "http://" if the end-user just enter a domain name. + * + * @param string $url Url + * @return string + */ + public function prependScheme($url) + { + if (!preg_match('%^https?://%', $url)) { + $url = 'http://'.$url; + } + + return $url; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/ReaderException.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/ReaderException.php new file mode 100644 index 0000000..4f03dbe --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Reader/ReaderException.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://combat.blog.lemonde.fr/2013/08/31/teddy-riner-le-rookie-devenu-rambo/#xtor=RSS-3208', + 'body' => array( + '//div[@class="entry-content"]', + ), + 'strip' => array( + '//*[contains(@class, "fb-like") or contains(@class, "social")]' + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.blogs.nytimes.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.blogs.nytimes.com.php new file mode 100644 index 0000000..ee641b0 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.blogs.nytimes.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'title' => '//header/h1', + 'test_url' => 'http://bits.blogs.nytimes.com/2012/01/16/wikipedia-plans-to-go-dark-on-wednesday-to-protest-sopa/', + 'body' => array( + '//div[@class="postContent"]', + ), + 'strip' => array( + '//*[@class="shareToolsBox"]', + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.igen.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.igen.fr.php new file mode 100644 index 0000000..f2028f4 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.igen.fr.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.igen.fr/ailleurs/2014/05/nvidia-va-delaisser-les-smartphones-grand-public-86031', + 'body' => array( + '//div[contains(@class, "field-name-body")]' + ), + 'strip' => array( + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.nytimes.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.nytimes.com.php new file mode 100644 index 0000000..8ff921c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.nytimes.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nytimes.com/2011/05/15/world/middleeast/15prince.html', + 'body' => array( + '//p[contains(@class, "story-content")] | //div[@class="image"]', + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.over-blog.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.over-blog.com.php new file mode 100644 index 0000000..cc5d83c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.over-blog.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://eliascarpe.over-blog.com/2015/12/re-upload-projets-d-avenir.html', + 'body' => array( + '//div[contains(concat(" ", normalize-space(@class), " "), " ob-section ")]', + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.phoronix.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.phoronix.com.php new file mode 100644 index 0000000..66713f7 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.phoronix.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.phoronix.com/scan.php?page=article&item=amazon_ec2_bare&num=1', + 'body' => array( + '//div[@class="content"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.slate.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.slate.com.php new file mode 100644 index 0000000..a795bca --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.slate.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.slate.com/articles/business/moneybox/2013/08/microsoft_ceo_steve_ballmer_retires_a_firsthand_account_of_the_company_s.html', + 'body' => array( + '//div[@class="sl-art-body"]', + ), + 'strip' => array( + '//*[contains(@class, "social") or contains(@class, "comments") or contains(@class, "sl-article-floatin-tools") or contains(@class, "sl-art-pag")]', + '//*[@id="mys_slate_logged_in"]', + '//*[@id="sl_article_tools_myslate_bottom"]', + '//*[@id="mys_myslate"]', + '//*[@class="sl-viral-container"]', + '//*[@class="sl-art-creds-cntr"]', + '//*[@class="sl-art-ad-midflex"]', + ) + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.theguardian.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.theguardian.com.php new file mode 100644 index 0000000..e0d6f3f --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.theguardian.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.theguardian.com/sustainable-business/2015/feb/02/2015-hyper-transparency-global-business', + 'body' => array( + '//div[contains(@class, "content__main-column--article")]', + ), + 'strip' => array( + '//div[contains(@class, "meta-container")]', + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wikipedia.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wikipedia.org.php new file mode 100644 index 0000000..7b8f76e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wikipedia.org.php @@ -0,0 +1,29 @@ + array( + '%.*%' => array( + 'test_url' => 'https://en.wikipedia.org/wiki/Grace_Hopper', + 'body' => array( + '//div[@id="bodyContent"]', + ), + 'strip' => array( + "//div[@id='toc']", + "//div[@id='catlinks']", + "//div[@id='jump-to-nav']", + "//div[@class='thumbcaption']//div[@class='magnify']", + "//table[@class='navbox']", + "//table[contains(@class, 'infobox')]", + "//div[@class='dablink']", + "//div[@id='contentSub']", + "//div[@id='siteSub']", + "//table[@id='persondata']", + "//table[contains(@class, 'metadata')]", + "//*[contains(@class, 'noprint')]", + "//*[contains(@class, 'printfooter')]", + "//*[contains(@class, 'editsection')]", + "//*[contains(@class, 'error')]", + "//span[@title='pronunciation:']", + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wired.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wired.com.php new file mode 100644 index 0000000..952b09a --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wired.com.php @@ -0,0 +1,44 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.wired.com/gamelife/2013/09/ouya-free-the-games/', + 'body' => array( + '//div[@data-js="gallerySlides"]', + '//div[starts-with(@class,"post")]', + ), + 'strip' => array( + '//h1', + '//nav', + '//button', + '//figure[starts-with(@class,"rad-slide")]', + '//figure[starts-with(@class,"end-slate")]', + '//div[contains(@class,"mobile-")]', + '//div[starts-with(@class,"mob-gallery-launcher")]', + '//div[contains(@id,"mobile-")]', + '//span[contains(@class,"slide-count")]', + '//div[contains(@class,"show-ipad")]', + '//img[contains(@id,"-hero-bg")]', + '//div[@data-js="overlayWrap"]', + '//ul[contains(@class,"metadata")]', + '//div[@class="opening center"]', + '//p[contains(@class="byline-mob"]', + '//div[@id="o-gallery"]', + '//div[starts-with(@class,"sm-col")]', + '//div[contains(@class,"pad-b-huge")]', + '//a[contains(@class,"visually-hidden")]', + '//*[@class="social"]', + '//i', + '//div[@data-js="mobGalleryAd"]', + '//div[contains(@class,"footer")]', + '//div[contains(@data-js,"fader")]', + '//div[@id="sharing"]', + '//div[contains(@id,"related")]', + '//div[@id="most-pop"]', + '//ul[@id="article-tags"]', + '//style', + '//section[contains(@class,"footer")]' + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wsj.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wsj.com.php new file mode 100644 index 0000000..f6e6cc1 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/.wsj.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://online.wsj.com/article/SB10001424127887324108204579023143974408428.html', + 'body' => array( + '//div[@class="articlePage"]', + ), + 'strip' => array( + '//*[@id="articleThumbnail_2"]', + '//*[@class="socialByline"]', + ) + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/01net.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/01net.com.php new file mode 100644 index 0000000..6d144f0 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/01net.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.01net.com/editorial/624550/twitter-rachete-madbits-un-specialiste-francais-de-lanalyse-dimages/', + 'body' => array( + '//div[@class="article_ventre_box"]', + ), + 'strip' => array( + '//link', + '//*[contains(@class, "article_navigation")]', + '//h1', + '//*[contains(@class, "article_toolbarMain")]', + '//*[contains(@class, "article_imagehaute_box")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/24.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/24.hu.php new file mode 100644 index 0000000..6c269db --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/24.hu.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://24.hu/belfold/2017/10/20/millios-lehuzasok-miatt-razziaztak-egy-budapesti-barban-videoval/', + 'body' => array( + '//div[@class="post-title-wrapper"]/h1', + '//div[@class="post-header-wrapper has-img"]/img', + '//div[@class="post-body"]' + + ), + 'strip' => array( + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/444.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/444.hu.php new file mode 100644 index 0000000..e65bad1 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/444.hu.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'https://444.hu/2017/10/20/tulszamlazo-helyen-utottek-rajta-budapest-belvarosaban', + 'body' => array( + '//div[@id="headline"]/h1', + '//article' + ), + 'strip' => array( + '//article/footer', + '//article/div[@class="jeti-roadblock ad"]', + '//figcaption', + '//noscript', + '//ul[@class="widget-stream-items"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/888.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/888.hu.php new file mode 100644 index 0000000..68cfe05 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/888.hu.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'https://888.hu/article-a-budapesti-szocik-nem-szeretik-a-videki-szocikat', + 'body' => array( + '//div[@id="cikkholder"]/h1', + '//div[@class="maincontent8"]' + ), + 'strip' => array( + '//div[@class="AdW"]', + '//h1[@class="olvastadmar"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/abstrusegoose.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/abstrusegoose.com.php new file mode 100644 index 0000000..752d041 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/abstrusegoose.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%alt="(.+)" title="(.+)" */>%' => '/>
    $1
    $2', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/achgut.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/achgut.com.php new file mode 100644 index 0000000..1e61fe6 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/achgut.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.achgut.com/artikel/deutscher_herbst_wg_reichsstrasse_106', + 'body' => array( + '//div[@class="headerpict_half"]/div/img', + '//div[@class="beitrag"]/div[@class="teaser_blog_text"]' + ), + 'strip' => array( + '//div[@class="footer_blog_text"]', + '//div[@class="beitrag"]/div[@class="teaser_blog_text"]/h2[1]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/adventuregamers.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/adventuregamers.com.php new file mode 100644 index 0000000..98d384e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/adventuregamers.com.php @@ -0,0 +1,23 @@ + array( + '%^/news.*%' => array( + 'test_url' => 'http://www.adventuregamers.com/news/view/31079', + 'body' => array( + '//div[@class="bodytext"]', + ) + ), + '%^/videos.*%' => array( + 'test_url' => 'http://www.adventuregamers.com/videos/view/31056', + 'body' => array( + '//iframe', + ) + ), + '%^/articles.*%' => array( + 'test_url' => 'http://www.adventuregamers.com/articles/view/31049', + 'body' => array( + '//div[@class="cleft"]', + ) + ) + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/alainonline.net.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/alainonline.net.php new file mode 100644 index 0000000..f440b23 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/alainonline.net.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.alainonline.net/news_details.php?lang=arabic&sid=18907', + 'body' => array( + '//div[@class="news_details"]', + ), + 'strip' => array( + '//div[@class="news_details"]/div/div[last()]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/aljazeera.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/aljazeera.com.php new file mode 100644 index 0000000..c02eb21 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/aljazeera.com.php @@ -0,0 +1,25 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.aljazeera.com/news/2015/09/xi-jinping-seattle-china-150922230118373.html', + 'body' => array( + '//article[@id="main-story"]', + ), + 'strip' => array( + '//script', + '//header', + '//ul', + '//section[contains(@class,"profile")]', + '//a[@target="_self"]', + '//div[contains(@id,"_2")]', + '//div[contains(@id,"_3")]', + '//img[@class="viewMode"]', + '//table[contains(@class,"in-article-item")]', + '//div[@data-embed-type="Brightcove"]', + '//div[@class="QuoteContainer"]', + '//div[@class="BottomByLine"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allafrica.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allafrica.com.php new file mode 100644 index 0000000..e8a506d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allafrica.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.aljazeera.com/news/2015/09/xi-jinping-seattle-china-150922230118373.html', + 'body' => array( + '//div[@class="story-body"]', + ), + 'strip' => array( + '//p[@class="kindofstory"]', + '//cite[@class="byline"]', + '//div[@class="useful-top"]', + '//div[contains(@class,"related-topics")]', + '//links', + '//sharebar', + '//related-topics', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allgemeine-zeitung.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allgemeine-zeitung.de.php new file mode 100644 index 0000000..8ede99b --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/allgemeine-zeitung.de.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.allgemeine-zeitung.de/lokales/polizei/mainz-gonsenheim-unbekannte-rauben-esso-tankstelle-in-kurt-schumacher-strasse-aus_14913147.htm', + 'body' => array( + '//div[contains(@class, "article")][1]', + ), + 'strip' => array( + '//read/h1', + '//*[@id="t-map"]', + '//*[contains(@class, "modules")]', + '//*[contains(@class, "adsense")]', + '//*[contains(@class, "linkbox")]', + '//*[contains(@class, "info")]', + '//*[@class="skip"]', + '//*[@class="funcs"]', + '//span[@class="nd address"]', + '//a[contains(@href, "abo-und-services")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/amazingsuperpowers.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/amazingsuperpowers.com.php new file mode 100644 index 0000000..3214c62 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/amazingsuperpowers.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/anythingcomic.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/anythingcomic.com.php new file mode 100644 index 0000000..51247f7 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/anythingcomic.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@id="comic_image"]', + '//div[@class="comment-wrapper"][position()=1]', + ), + 'strip' => array(), + 'test_url' => 'http://www.anythingcomic.com/comics/2108929/stress-free/', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ap.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ap.org.php new file mode 100644 index 0000000..5bb2bb6 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ap.org.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://hosted.ap.org/dynamic/stories/A/AS_CHINA_GAO_ZHISHENG?SITE=AP&SECTION=HOME&TEMPLATE=DEFAULT', + 'body' => array( + '//img[@class="ap-smallphoto-img"]', + '//span[@class="entry-content"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/areadvd.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/areadvd.de.php new file mode 100644 index 0000000..fc56922 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/areadvd.de.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.areadvd.de/news/daily-deals-angebote-bei-lautsprecher-teufel-3/', + 'body' => array('//div[contains(@class,"entry")]'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/arstechnica.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/arstechnica.com.php new file mode 100644 index 0000000..55e01ce --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/arstechnica.com.php @@ -0,0 +1,25 @@ + array( + '%.*%' => array( + 'test_url' => 'http://arstechnica.com/tech-policy/2015/09/judge-warners-2m-happy-birthday-copyright-is-bogus/', + 'body' => array( + '//article', + ), + 'strip' => array( + '//h4[@class="post-upperdek"]', + '//h1', + '//ul[@class="lSPager lSGallery"]', + '//div[@class="lSAction"]', + '//section[@class="post-meta"]', + '//figcaption', + '//aside', + '//div[@class="gallery-image-credit"]', + '//section[@class="article-author"]', + '//*[contains(@id,"social-")]', + '//div[contains(@id,"footer")]', + ), + ), + ), +); + diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/atv.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/atv.hu.php new file mode 100644 index 0000000..170ec4d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/atv.hu.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.atv.hu/belfold/20171020-tobb-millio-forintot-csalt-ki-egy-idos-ferfitol-a-budapesti-no', + 'body' => array( + '//article' + ), + 'strip' => array( + '//span[@class="date"]', + '//div[@class="fb-like db_iframe_widget"]', + '//div[@class="ad-wrapper dashed-border"]', + '//div[@class="footer-meta-wrapper"]', + '//div[@class="image-wrapper "]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/awkwardzombie.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/awkwardzombie.com.php new file mode 100644 index 0000000..5ab7051 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/awkwardzombie.com.php @@ -0,0 +1,10 @@ + array( + '%/index.php.*comic=.*%' => array( + 'test_url' => 'http://www.awkwardzombie.com/index.php?comic=041315', + 'body' => array('//*[@id="comic"]/img'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/backchannel.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/backchannel.com.php new file mode 100644 index 0000000..bc5932a --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/backchannel.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'https://medium.com/lessons-learned/917b8b63ae3e', + 'body' => array( + '//div[contains(@class,"section-inner")]', + ), + 'strip' => array( + '//div[contains(@class,"metabar")]', + '//img[contains(@class,"thumbnail")]', + '//h1', + '//blockquote', + '//p[contains(@class,"graf-after--h4")]' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bangkokpost.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bangkokpost.com.php new file mode 100644 index 0000000..165515b --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bangkokpost.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bangkokpost.com/news/politics/704204/new-us-ambassador-arrives-in-bangkok', + 'body' => array( + '//article/div[@class="articleContents"]', + ), + 'strip' => array( + '//h2', + '//h4', + '//div[@class="text-size"]', + '//div[@class="relate-story"]', + '//div[@class="text-ads"]', + '//ul', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bauerwilli.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bauerwilli.com.php new file mode 100644 index 0000000..b191a6e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bauerwilli.com.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bauerwilli.com/intuitive-eating/', + 'body' => array( + '//div[@class="entry-thumbnail"]', + '//div[@class="entry-content"]', + ), + 'strip' => array( + '//div[@class="tptn_counter"]', + '//div[contains(@class, "sharedaddy")]' + ), + ), + ), +); + diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bgr.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bgr.com.php new file mode 100644 index 0000000..7507a2f --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bgr.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://bgr.com/2015/09/27/iphone-6s-waterproof-testing/', + 'body' => array( + '//img[contains(@class,"img")]', + '//div[@class="text-column"]', + ), + 'strip' => array( + '//strong', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigfootjustice.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigfootjustice.com.php new file mode 100644 index 0000000..d06ed12 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigfootjustice.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigpicture.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigpicture.ru.php new file mode 100644 index 0000000..55c4089 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bigpicture.ru.php @@ -0,0 +1,31 @@ + array( + '%.*%' => array( + 'test_url' => 'http://bigpicture.ru/?p=556658', + 'body' => array( + '//div[@class="article container"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//h1', + '//*[@class="wp-smiley"]', + '//div[@class="ipmd"]', + '//div[@class="tags"]', + '//div[@class="social-button"]', + '//div[@class="bottom-share"]', + '//div[@class="raccoonbox"]', + '//div[@class="yndadvert"]', + '//div[@class="we-recommend"]', + '//div[@class="relap-bigpicture_ru-wrapper"]', + '//div[@id="mmail"]', + '//div[@id="mobile-ads-cut"]', + '//div[@id="liquidstorm-alt-html"]', + '//div[contains(@class, "post-tags")]', + '//*[contains(text(),"Смотрите также")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bizjournals.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bizjournals.com.php new file mode 100644 index 0000000..d1cc3da --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bizjournals.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bizjournals.com/milwaukee/news/2015/09/30/bucks-will-hike-prices-on-best-seats-at-new-arena.html', + 'body' => array( + '//figure/div/a/img', + '//p[@class="content__segment"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/biztimes.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/biztimes.com.php new file mode 100644 index 0000000..d21aa98 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/biztimes.com.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.biztimes.com/2017/02/10/settlement-would-revive-fowler-lake-condo-project-in-oconomowoc/', + 'body' => array( + '//h2/span[@class="subhead"]', + '//div[contains(@class,"article-content")]', + ), + 'strip' => array( + '//script', + '//div[contains(@class,"mobile-article-content")]', + '//div[contains(@class,"sharedaddy")]', + '//div[contains(@class,"author-details")]', + '//div[@class="row ad"]', + '//div[contains(@class,"relatedposts")]', + '//div[@class="col-lg-12"]', + '//div[contains(@class,"widget")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bleepingcomputer.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bleepingcomputer.com.php new file mode 100644 index 0000000..7b74060 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bleepingcomputer.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.bleepingcomputer.com/news/google/chromes-sandbox-feature-infringes-on-three-patents-so-google-must-now-pay-20m/', + 'body' => array( + '//div[@class="article_section"]', + ), + 'strip' => array( + '//*[@itemprop="headline"]', + '//div[@class="cz-news-story-title-section"]' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.fefe.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.fefe.de.php new file mode 100644 index 0000000..39c88ae --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.fefe.de.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://blog.fefe.de/?ts=ad706a73', + 'body' => array( + '/html/body/ul', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.mapillary.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.mapillary.com.php new file mode 100644 index 0000000..ce01651 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/blog.mapillary.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://blog.mapillary.com/update/2015/08/26/traffic-sign-updates.html', + 'body' => array( + '//div[contains(@class, "blog-post__content")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/brewers.mlb.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/brewers.mlb.com.php new file mode 100644 index 0000000..be406fa --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/brewers.mlb.com.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://m.brewers.mlb.com/news/article/161364798', + 'body' => array( + '//article[contains(@class,"article")]', + ), + 'strip' => array( + '//div[contains(@class,"ad-slot")]', + '//h1', + '//span[@class="timestamp"]', + '//div[contains(@class,"contributor-bottom")]', + '//div[contains(@class,"video")]', + '//ul[contains(@class,"social")]', + '//p[@class="tagline"]', + '//div[contains(@class,"social")]', + '//div[@class="button-wrap"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buenosairesherald.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buenosairesherald.com.php new file mode 100644 index 0000000..4e73e79 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buenosairesherald.com.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.buenosairesherald.com/article/199344/manzur-named-next-governor-of-tucum%C3%A1n', + 'body' => array( + '//div[@style="float:none"]', + ), + 'strip' => array( + '//div[contains(@class, "bz_alias_short_desc_container"]', + '//td[@id="bz_show_bug_column_1"]', + '//table[@id="attachment_table"]', + '//table[@class="bz_comment_table"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bunicomic.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bunicomic.com.php new file mode 100644 index 0000000..ad83e43 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/bunicomic.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bunicomic.com/comic/buni-623/', + 'body' => array( + '//div[@class="comic-table"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buttersafe.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buttersafe.com.php new file mode 100644 index 0000000..1f313cd --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/buttersafe.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://buttersafe.com/2015/04/21/the-incredible-flexible-man/', + 'body' => array( + '//div[@id="comic"]', + '//div[@class="post-comic"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cad-comic.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cad-comic.com.php new file mode 100644 index 0000000..a631c97 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cad-comic.com.php @@ -0,0 +1,12 @@ + array( + '%/cad/.+%' => array( + 'test_url' => 'http://www.cad-comic.com/cad/20150417', + 'body' => array( + '//*[@id="content"]/img', + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chaoslife.findchaos.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chaoslife.findchaos.com.php new file mode 100644 index 0000000..ea6191e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chaoslife.findchaos.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://chaoslife.findchaos.com/pets-in-the-wild', + 'body' => array('//div[@id="comic"]'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chinafile.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chinafile.com.php new file mode 100644 index 0000000..450117b --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/chinafile.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.chinafile.com/books/shanghai-faithful?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+chinafile%2FAll+%28ChinaFile%29', + 'body' => array( + '//div[contains(@class,"pane-featured-photo-panel-pane-1")]', + '//div[contains(@class,"video-above-fold")]', + '//div[@class="sc-media"]', + '//div[contains(@class,"field-name-body")]', + ), + 'strip' => array( + '//div[contains(@class,"cboxes")]', + '//div[contains(@class,"l-middle")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cicero.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cicero.de.php new file mode 100644 index 0000000..2cd1b70 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cicero.de.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'https://cicero.de/innenpolitik/plaene-der-eu-kommission-der-ganz-normale-terror', + 'body' => array( + '//p[@class="lead"]', + '//article/div[2]/div[contains(@class, "field--name-field-cc-image")]', + '//article/div[2]/div[contains(@class, "image-description")]', + '//div[@class="field field-name-field-cc-body"]', + ), + 'strip' => array( + '//*[contains(@class, "urban-ad-sign")]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cliquerefresh.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cliquerefresh.com.php new file mode 100644 index 0000000..9dcc7e5 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cliquerefresh.com.php @@ -0,0 +1,10 @@ + array( + '%/comic.*%' => array( + 'test_url' => 'http://cliquerefresh.com/comic/078-stating-the-obvious/', + 'body' => array('//div[@class="comicImg"]/img | //div[@class="comicImg"]/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cnet.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cnet.com.php new file mode 100644 index 0000000..60767a5 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cnet.com.php @@ -0,0 +1,37 @@ + array( + '%^/products.*%' => array( + 'test_url' => 'http://www.cnet.com/products/fibaro-flood-sensor/#ftag=CADf328eec', + 'body' => array( + '//li[contains(@class,"slide first"] || //figure[contains(@class,(promoFigure))]', + '//div[@class="quickInfo"]', + '//div[@class="col-6 ratings"]', + '//div[@id="editorReview"]', + ), + 'strip' => array( + '//script', + '//a[@class="clickToEnlarge"]', + '//div[@section="topSharebar"]', + '//div[contains(@class,"related")]', + '//div[contains(@class,"ad-")]', + '//div[@section="shortcodeGallery"]', + ), + ), + '%.*%' => array( + 'test_url' => 'http://cnet.com.feedsportal.com/c/34938/f/645093/s/4a340866/sc/28/l/0L0Scnet0N0Cnews0Cman0Eclaims0Eonline0Epsychic0Emade0Ehim0Ebuy0E10Emillion0Epowerball0Ewinning0Eticket0C0Tftag0FCAD590Aa51e/story01.htm', + 'body' => array( + '//p[@itemprop="description"]', + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//script', + '//a[@class="clickToEnlarge"]', + '//div[@section="topSharebar"]', + '//div[contains(@class,"related")]', + '//div[contains(@class,"ad-")]', + '//div[@section="shortcodeGallery"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/coinwelt.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/coinwelt.de.php new file mode 100644 index 0000000..28a8bba --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/coinwelt.de.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://coinwelt.de/2017/08/bitcache-kreierer-kim-dotcom-bietet-arbeitsplaetze-fuer-blockchain-goetter/', + 'body' => array( + '//div[@class="post-inner"]//div[@class="entry"]', + ), + 'strip' => array( + '//div[contains(@class, "shariff")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/consomac.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/consomac.fr.php new file mode 100644 index 0000000..9209f9c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/consomac.fr.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://consomac.fr/news-2430-l-iphone-6-toujours-un-secret-bien-garde.html', + 'body' => array( + '//div[contains(@id, "newscontent")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cowbirdsinlove.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cowbirdsinlove.com.php new file mode 100644 index 0000000..3214c62 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/cowbirdsinlove.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/crash.net.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/crash.net.php new file mode 100644 index 0000000..88cef14 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/crash.net.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.crash.net/motogp/news/885102/1/dovizioso-mugello-win-was-catalyst-for-title-challenge', + 'body' => array( + '//*[@id="block-system-main"]', + ), + 'strip' => array( + '//script', + '//style', + '//*[@class="social-bar"]', + '//*[@id="below-headline-image-ad"]', + '//*[@class="advert-"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/csmonitor.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/csmonitor.com.php new file mode 100644 index 0000000..481e4b0 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/csmonitor.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.csmonitor.com/USA/Politics/2015/0925/John-Boehner-steps-down-Self-sacrificing-but-will-it-lead-to-better-government', + 'body' => array( + '//h2[@id="summary"]', + '//div[@class="flex-video youtube"]', + '//div[contains(@class,"eza-body")]', + ), + 'strip' => array( + '//span[@id="breadcrumb"]', + '//div[@id="byline-wrapper"]', + '//div[@class="injection"]', + '//*[contains(@class,"promo_link")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyjs.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyjs.com.php new file mode 100644 index 0000000..20eb1d7 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyjs.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://dailyjs.com/2014/08/07/p5js/', + 'body' => array( + '//div[@id="post"]', + ), + 'strip' => array( + '//h2[@class="post"]', + '//div[@class="meta"]', + '//*[contains(@class, "addthis_toolbox")]', + '//*[contains(@class, "addthis_default_style")]', + '//*[@class="navigation small"]', + '//*[@id="related"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyreporter.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyreporter.com.php new file mode 100644 index 0000000..db3fc0e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailyreporter.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://dailyreporter.com/2016/01/09/us-supreme-court-case-could-weaken-government-workers-unions/', + 'body' => array( + '//div[contains(@class, "entry-content")]', + ), + 'strip' => array( + '//div[@class="dmcss_login_form"]', + '//*[contains(@class, "sharedaddy")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailytech.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailytech.com.php new file mode 100644 index 0000000..5d1df4a --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dailytech.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.dailytech.com/Apples+First+Fixes+to+iOS+9+Land+w+iOS++901+Release/article37495.htm', + 'body' => array( + '//div[@class="NewsBodyImage"]', + '//span[@id="lblSummary"]', + '//span[@id="lblBody"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/degroupnews.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/degroupnews.com.php new file mode 100644 index 0000000..91f5c56 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/degroupnews.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.degroupnews.com/medias/vodsvod/amazon-concurrence-la-chromecast-de-google-avec-fire-tv-stick', + 'body' => array( + '//div[@class="contenu"]', + ), + 'strip' => array( + '//div[contains(@class, "a2a")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/derstandard.at.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/derstandard.at.php new file mode 100644 index 0000000..7e95a51 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/derstandard.at.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://derstandard.at/2000010267354/The-Witcher-3-Hohe-Hardware-Anforderungen-fuer-PC-Spieler?ref=rss', + 'body' => array( + '//div[@class="copytext"]', + '//ul[@id="media-list"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dilbert.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dilbert.com.php new file mode 100644 index 0000000..b8e9b3d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dilbert.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@class="img-responsive img-comic"]', + ), + 'test_url' => 'http://dilbert.com/strip/2016-01-28', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/discovermagazine.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/discovermagazine.com.php new file mode 100644 index 0000000..ae0dfe7 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/discovermagazine.com.php @@ -0,0 +1,26 @@ + array( + '%.*%' => array( + 'test_url' => 'http://blogs.discovermagazine.com/neuroskeptic/2017/01/25/publishers-jeffrey-beall/', + 'body' => array( + '//div[@class="contentWell"]', + ), + 'strip' => array( + '//h1', + '//div[@class="breadcrumbs"]', + '//div[@class="mobile"]', + '//div[@class="fromIssue"]', + '//div[contains(@class,"belowDeck")]', + '//div[@class="meta"]', + '//div[@class="shareIcons"]', + '//div[@class="categories"]', + '//div[@class="navigation"]', + '//div[@class="heading"]', + '//div[contains(@id,"-ad")]', + '//div[@class="relatedArticles"]', + '//div[@id="disqus_thread"]' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/distrowatch.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/distrowatch.com.php new file mode 100644 index 0000000..aefc8f8 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/distrowatch.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://distrowatch.com/?newsid=08355', + 'body' => array( + '//td[@class="NewsText"][1]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dozodomo.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dozodomo.com.php new file mode 100644 index 0000000..e116695 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/dozodomo.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://dozodomo.com/bento/2014/03/04/lart-des-maki-de-takayo-kiyota/', + 'body' => array( + '//div[@class="joke"]', + '//div[@class="story-cover"]', + '//div[@class="story-content"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/drawingboardcomic.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/drawingboardcomic.com.php new file mode 100644 index 0000000..cd30f2e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/drawingboardcomic.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'body' => array('//img[@id="comicimage"]'), + 'strip' => array(), + 'test_url' => 'http://drawingboardcomic.com/index.php?comic=208', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/e-w-e.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/e-w-e.ru.php new file mode 100644 index 0000000..8139cc9 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/e-w-e.ru.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://e-w-e.ru/16-prekrasnyx-izobretenij-zhenshhin/', + 'body' => array( + '//div[contains(@class, "post_text")]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[@class="views_post"]', + '//*[@class="adman_mobile"]', + '//*[@class="adman_desctop"]', + '//*[contains(@rel, "nofollow")]', + '//*[contains(@class, "wp-smiley")]', + '//*[contains(text(),"Источник:")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/economist.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/economist.com.php new file mode 100644 index 0000000..522032f --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/economist.com.php @@ -0,0 +1,25 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.economist.com/blogs/buttonwood/2017/02/mixed-signals?fsrc=rss', + 'body' => array( + '//article', + ), + 'strip' => array( + '//span[@class="blog-post__siblings-list-header "]', + '//h1', + '//aside', + '//div[@class="blog-post__asideable-wrapper"]', + '//div[@class="share_inline_header"]', + '//div[@id="column-right"]', + '//div[contains(@class,"blog-post__siblings-list-aside")]', + '//div[@class="video-player__wrapper"]', + '//div[@class="blog-post__bottom-panel"]', + '//div[contains(@class,"latest-updates-panel__container")]', + '//div[contains(@class,"blog-post__asideable-content")]', + '//div[@aria-label="Advertisement"]' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/encyclopedie.naheulbeuk.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/encyclopedie.naheulbeuk.com.php new file mode 100644 index 0000000..19bcbde --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/encyclopedie.naheulbeuk.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://encyclopedie.naheulbeuk.com/article.php3?id_article=352', + 'body' => array( + '//td//h1[@class="titre-texte"]', + '//td//div[@class="surtitre"]', + '//td//div[@class="texte"]', + ), + ) + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/endlessorigami.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/endlessorigami.com.php new file mode 100644 index 0000000..d06ed12 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/endlessorigami.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/engadget.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/engadget.com.php new file mode 100644 index 0000000..cf9e448 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/engadget.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.engadget.com/2015/04/20/dark-matter-discovery/?ncid=rss_truncated', + 'body' => array('//div[@id="page_body"]/div[@class="container@m-"]'), + 'strip' => array('//aside[@role="banner"]'), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/escapistmagazine.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/escapistmagazine.com.php new file mode 100644 index 0000000..e86b59c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/escapistmagazine.com.php @@ -0,0 +1,45 @@ + array( + '%/articles/view/comicsandcosplay/comics/critical-miss.*%' => array( + 'body' => array('//*[@class="body"]/span/img | //div[@class="folder_nav_links"]/following::p'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/critical-miss/13776-Critical-Miss-on-Framerates?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/namegame.*%' => array( + 'body' => array('//*[@class="body"]/span/p/img[@height != "120"]'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/namegame/9759-Leaving-the-Nest?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/stolen-pixels.*%' => array( + 'body' => array('//*[@class="body"]/span/p[2]/img'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/stolen-pixels/8866-Stolen-Pixels-258-Where-the-Boys-Are?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/bumhugparade.*%' => array( + 'body' => array('//*[@class="body"]/span/p[2]/img'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/bumhugparade/8262-Bumhug-Parade-13?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay.*/comics/escapistradiotheater%' => array( + 'body' => array('//*[@class="body"]/span/p[2]/img'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/escapistradiotheater/8265-The-Escapist-Radio-Theater-13?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/paused.*%' => array( + 'body' => array('//*[@class="body"]/span/p[2]/img | //*[@class="body"]/span/div/img'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/paused/8263-Paused-16?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/comicsandcosplay/comics/fraughtwithperil.*%' => array( + 'body' => array('//*[@class="body"]'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/comicsandcosplay/comics/fraughtwithperil/12166-The-Escapist-Presents-Escapist-Comics-Critical-Miss-B-lyeh-Fhlop?utm_source=rss&utm_medium=rss&utm_campaign=articles', + 'strip' => array(), + ), + '%/articles/view/video-games/columns/.*%' => array( + 'body' => array('//*[@id="article_content"]'), + 'test_url' => 'http://www.escapistmagazine.com/articles/view/video-games/columns/experienced-points/13971-What-50-Shades-and-Batman-Have-in-Common.2', + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/espn.go.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/espn.go.com.php new file mode 100644 index 0000000..76a20f7 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/espn.go.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://espn.go.com/nfl/story/_/id/13388208/jason-whitlock-chip-kelly-controversy', + 'body' => array( + '//p', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/exocomics.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/exocomics.com.php new file mode 100644 index 0000000..5adc59f --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/exocomics.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'body' => array('//a[@class="comic"]/img'), + 'strip' => array(), + 'test_url' => 'http://www.exocomics.com/379', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/explosm.net.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/explosm.net.php new file mode 100644 index 0000000..3fdf02c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/explosm.net.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://explosm.net/comics/3803/', + 'body' => array( + '//div[@id="comic-container"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/extrafabulouscomics.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/extrafabulouscomics.com.php new file mode 100644 index 0000000..12697cc --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/extrafabulouscomics.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/factroom.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/factroom.ru.php new file mode 100644 index 0000000..a572061 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/factroom.ru.php @@ -0,0 +1,27 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.factroom.ru/life/20-facts-about-oil', + 'body' => array( + '//div[@class="post"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//h1', + '//div[@id="yandex_ad2"]', + '//*[@class="jp-relatedposts"]', + '//div[contains(@class, "likely-desktop")]', + '//div[contains(@class, "likely-mobile")]', + '//p[last()]', + '//div[contains(@class, "facebook")]', + '//div[contains(@class, "desktop-underpost-direct")]', + '//div[contains(@class, "source-box")]', + '//div[contains(@class, "under-likely-desktop")]', + '//div[contains(@class, "mobile-down-post")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcodesign.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcodesign.com.php new file mode 100644 index 0000000..74e70a8 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcodesign.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.fastcodesign.com/3026548/exposure/peek-inside-the-worlds-forbidden-subway-tunnels', + 'body' => array( + '//article[contains(@class, "body prose")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcoexist.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcoexist.com.php new file mode 100644 index 0000000..6916f28 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcoexist.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.fastcoexist.com/3026114/take-a-seat-on-this-gates-funded-future-toilet-that-will-change-how-we-think-about-poop', + 'body' => array( + '//article[contains(@class, "body prose")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcompany.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcompany.com.php new file mode 100644 index 0000000..e0869a2 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fastcompany.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.fastcompany.com/3026712/fast-feed/elon-musk-an-apple-tesla-merger-is-very-unlikely', + 'body' => array( + '//article[contains(@class, "body prose")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ffworld.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ffworld.com.php new file mode 100644 index 0000000..20a47b2 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ffworld.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.ffworld.com/?rub=news&page=voir&id=2709', + 'body' => array( + '//div[@class="news_body"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/foreignpolicy.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/foreignpolicy.com.php new file mode 100644 index 0000000..3cbcddc --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/foreignpolicy.com.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://foreignpolicy.com/2016/01/09/networking-giant-pulls-nsa-linked-code-exploited-by-hackers/', + 'body' => array( + '//article', + ), + 'strip' => array( + '//div[@id="post-category"]', + '//div[@id="desktop-right"]', + '//h1', + '//section[@class="article-meta"]', + '//div[@class="side-panel-wrapper"]', + '//*[contains(@class, "share-")]', + '//*[contains(@id, "taboola-")]', + '//div[@class="comments"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fossbytes.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fossbytes.com.php new file mode 100644 index 0000000..6ce4725 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fossbytes.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://fossbytes.com/fbi-hacked-1000-computers-to-shut-down-largest-child-pornography-site-on-the-dark-web/', + 'body' => array( + '//div[@class="entry-inner"]', + ), + 'strip' => array( + '//*[@class="at-above-post addthis_default_style addthis_toolbox at-wordpress-hide"]', + '//*[@class="at-below-post addthis_default_style addthis_toolbox at-wordpress-hide"]', + '//*[@class="at-below-post-recommended addthis_default_style addthis_toolbox at-wordpress-hide"]', + '//*[@class="code-block code-block-12 ai-desktop"]', + '//*[@class="code-block code-block-13 ai-tablet-phone"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fototelegraf.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fototelegraf.ru.php new file mode 100644 index 0000000..ca2f85a --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fototelegraf.ru.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://fototelegraf.ru/?p=348232', + 'body' => array( + '//div[@class="post-content"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//div[@class="imageButtonsBlock"]', + '//div[@class="adOnPostBtwImg"]', + '//div[contains(@class, "post-tags")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fowllanguagecomics.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fowllanguagecomics.com.php new file mode 100644 index 0000000..3f62f07 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/fowllanguagecomics.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'body' => array('//*[@id="comic"] | //*[@class="post-image"]'), + 'strip' => array(), + 'test_url' => 'http://www.fowllanguagecomics.com/comic/working-out/', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamechannel.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamechannel.hu.php new file mode 100644 index 0000000..8ab9c5c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamechannel.hu.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.gamechannel.hu/cikk/hirblock/a-legacy-of-kain-feltamasztasara-keszul-a-crystal-dynamics', + 'body' => array( + '//div[@class="post"]/div[@class="entry"]' + ), + 'strip' => array( + '//div[@class="valaszto"]', + '//center/blockquote' // as we can't grab iframe here + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamestar.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamestar.hu.php new file mode 100644 index 0000000..56a4c72 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gamestar.hu.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.gamestar.hu/hir/horizon-zero-dawn-the-frozen-wilds-vedjegy-239019.html', + 'body' => array( + '//article/header/h1', + '//div[@class="section section-2-3"]/div[@class="image"]/img', + '//article/p[@class="lead"]', + '//article/div[@class="content"]' + ), + 'strip' => array( + '//div[@class="ad ad-article-inside"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geek.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geek.com.php new file mode 100644 index 0000000..d9ccecc --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geek.com.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.geek.com/news/the-11-best-ways-to-eat-eggs-1634076/', + 'body' => array( + '//div[@class="articleinfo"]/figure', + '//div[@class="articleinfo"]/article', + '//span[@class="by"]', + ), + 'strip' => array( + '//span[@class="red"]', + '//div[@class="on-target"]' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geektimes.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geektimes.ru.php new file mode 100644 index 0000000..1954138 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/geektimes.ru.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'https://geektimes.ru/post/289151/', + 'body' => array( + "//div[contains(concat(' ',normalize-space(@class),' '),' content ')]" + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gerbilwithajetpack.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gerbilwithajetpack.com.php new file mode 100644 index 0000000..44013b3 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gerbilwithajetpack.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'body' => array( + '//div[@id="comic-1"]', + '//div[@class="entry"]', + ), + 'test_url' => 'http://gerbilwithajetpack.com/passing-the-digital-buck/', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/giantitp.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/giantitp.com.php new file mode 100644 index 0000000..d9c3ae5 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/giantitp.com.php @@ -0,0 +1,12 @@ + array( + '%/comics/oots.*%' => array( + 'test_url' => 'http://www.giantitp.com/comics/oots0989.html', + 'body' => array( + '//td[@align="center"]/img', + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/github.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/github.com.php new file mode 100644 index 0000000..726634f --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/github.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'https://github.com/audreyr/favicon-cheat-sheet', + 'body' => array( + '//article[contains(@class, "entry-content")]', + ), + 'strip' => array( + '//h1', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gocomics.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gocomics.com.php new file mode 100644 index 0000000..32960f0 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gocomics.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.gocomics.com/pearlsbeforeswine/2015/05/30', + 'body' => array( + '//div[1]/p[1]/a[1]/img', + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/golem.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/golem.de.php new file mode 100644 index 0000000..8731285 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/golem.de.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.golem.de/news/breko-telekom-verzoegert-gezielt-den-vectoring-ausbau-1311-102974.html', + 'body' => array( + '//header[@class="cluster-header"]', + '//header[@class="paged-cluster-header"]', + '//div[@class="formatted"]', + ), + 'next_page' => array( + '//a[@id="atoc_next"]' + ), + 'strip' => array( + '//header[@class="cluster-header"]/a', + '//header[@class="cluster-header"]/h1', + '//div[@id="iqadtile4"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gondola.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gondola.hu.php new file mode 100644 index 0000000..62b1e3c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gondola.hu.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'https://gondola.hu/hirek/213754-A_budapesti_fejlesztesek_kerdese_mar_nem_zsakbamacska.html', + 'body' => array( + '//div[@id="cikk"]/div[@class="cim"]', + '//br[1]', + '//div[@class="alcim"]', + '//div[@class="lead"]', + '//div[@class="szoveg"]' + ), + 'strip' => array( + '//div[@class="ikonok"]', + '//div[@class="linkekblokk"]', + '//div[@id="billboardbanner"]', + '//div[@class="szerzo"]', + '//div[@class="kulcsszavak"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gorabbit.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gorabbit.ru.php new file mode 100644 index 0000000..4e43248 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/gorabbit.ru.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://gorabbit.ru/article/10-oshchushcheniy-za-rulem-kogda-tolko-poluchil-voditelskie-prava', + 'body' => array( + '//div[@class="detail_text"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//div[@class="socials"]', + '//div[@id="cr_1"]', + '//div[@class="related_items"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/habrahabr.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/habrahabr.ru.php new file mode 100644 index 0000000..3f1ec16 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/habrahabr.ru.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'https://habrahabr.ru/company/pentestit/blog/328606/', + 'body' => array( + "//div[contains(concat(' ',normalize-space(@class),' '),' content ')]" + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/happletea.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/happletea.com.php new file mode 100644 index 0000000..75b0b83 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/happletea.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'body' => array( + '//div[@id="comic"]', + '//div[@class="entry"]', + ), + 'strip' => array('//div[@class="ssba"]'), + 'test_url' => 'http://www.happletea.com/comic/mans-best-friend/', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hardware.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hardware.fr.php new file mode 100644 index 0000000..56aec4f --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hardware.fr.php @@ -0,0 +1,11 @@ + array( + '%^/news.*%' => array( + 'test_url' => 'http://www.hardware.fr/news/14760/intel-lance-nouveaux-ssd-nand-3d.html', + 'body' => array( + '//div[@class="content_actualite"]/div[@class="md"]', + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/heise.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/heise.de.php new file mode 100644 index 0000000..0ee6915 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/heise.de.php @@ -0,0 +1,79 @@ + array( + '%^/tp.*%' => array( + 'test_url' => 'https://www.heise.de/tp/features/Macrons-Vermoegenssteuer-Der-Staat-verzichtet-auf-3-2-Milliarden-3863931.html', + 'body' => array( + '//main/article' + ), + 'strip' => array( + '//header', + '//aside', + '//nav[@class="pre-akwa-toc"]', + '//*[@class="seite_zurueck"]', + '//*[@class="pagination"]', + '//a[@class="kommentare_lesen_link"]', + '//div[contains(@class, "shariff")]', + '//a[@class="beitragsfooter_permalink"]', + '//a[@class="beitragsfooter_fehlermelden"]', + '//a[@class="beitragsfooter_printversion"]' + ), + 'next_page' => array( + '//a[@class="seite_weiter"]' + ), + ), + '%^/newsticker/meldung.*%' => array( + 'test_url' => 'https://www.heise.de/newsticker/meldung/DragonFly-BSD-5-0-mit-experimentellem-HAMMER2-veroeffentlicht-3864731.html', + 'body' => array( + '//div[@class="article-content"]', + ), + 'strip' => array( + '//*[contains(@class, "gallery")]', + '//*[contains(@class, "video")]', + ), + ), + '%^/autos/artikel.*%' => array( + 'test_url' => 'https://www.heise.de/autos/artikel/Bericht-Mazda-baut-Range-Extender-mit-Wankelmotor-3864760.html', + 'body' => array( + '//section[@id="artikel_text"]' + ), + 'strip' => array( + '//p[@id="content_foren"]', + '//div[contains(@class, "shariff")]', + '//p[@class="permalink"]', + '//p[@class="printversion"]' + ), + ), + '%^/foto/meldung.*%' => array( + 'test_url' => 'https://www.heise.de/foto/meldung/Wildlife-Fotograf-des-Jahres-Gewinnerbild-zeigt-getoetetes-Nashorn-3864311.html', + 'body' => array( + '//div[@class="article-content"]' + ), + ), + '%^/ct.*%' => array( + 'test_url' => 'https://www.heise.de/ct/artikel/Google-Pixel-2-und-Pixel-2-XL-im-Test-3863842.html', + 'body' => array( + '//main/div[1]/div[1]/section' + ), + 'strip' => array( + '//header' + ) + ), + '%^/developer.*%' => array( + 'test_url' => 'https://www.heise.de/developer/meldung/Container-Docker-unterstuetzt-Kubernetes-3863625.html', + 'body' => array( + '//div[@class="article-content"]' + ) + ), + '%.*%' => array( + 'test_url' => 'https://www.heise.de/mac-and-i/meldung/iOS-App-Nude-findet-mittels-ML-Nacktbilder-und-versteckt-sie-3864217.html', + 'body' => array( + '//article/div[@class="meldung_wrapper"]', + ), + 'strip' => array( + '//*[contains(@class, "gallery")]', + '//*[contains(@class, "video")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hirek.prim.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hirek.prim.hu.php new file mode 100644 index 0000000..1a38809 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hirek.prim.hu.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://hirek.prim.hu/cikk/2017/10/02/atadtak_a_6_fenntarthatosagi_sajtodijat', + 'body' => array( + '//div[@class="boxbody article_box"]/h2', + '//div[@class="text_body"]' + ), + 'strip' => array( + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hotshowlife.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hotshowlife.com.php new file mode 100644 index 0000000..faf01f3 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hotshowlife.com.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'https://hotshowlife.com/top-10-chempionov-produktov-po-szhiganiyu-kalorij/', + 'body' => array( + '//div[@class="entry-content"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//div[@class="ads2"]', + '//div[@class="mistape_caption"]', + '//div[contains(@class, "et_social_media_hidden")]', + '//div[contains(@class, "et_social_inline_bottom")]', + '//div[contains(@class, "avatar")]', + '//ul[contains(@class, "entry-tags")]', + '//div[contains(@class, "entry-meta")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/huffingtonpost.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/huffingtonpost.com.php new file mode 100644 index 0000000..b52b07b --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/huffingtonpost.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.huffingtonpost.com/2014/02/20/centscere-social-media-syracuse_n_4823848.html', + 'body' => array( + '//article[@class="content")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hvg.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hvg.hu.php new file mode 100644 index 0000000..efc9371 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/hvg.hu.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://hvg.hu/brandchannel/mastercardbch_20171006_Egyetlen_mobillal_erintettuk_Budapest_legjobb_gasztrohelyeit', + 'body' => array( + '//div[@class="article-title article-title"]', + '//div[@class="article-cover-img"]', + '//div[@class="article-main"]' + ), + 'strip' => array( + '//figcaption', + '//div[@class="article-info byline"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/idokep.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/idokep.hu.php new file mode 100644 index 0000000..06cae09 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/idokep.hu.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.idokep.hu/hirek/4-es-erossegu-tajfun-tart-japan-fele', + 'body' => array( + '//div[@class="cikk-title"]/h3', + '//div[@class="lead"]', + '//div[@class="atvett_tartalom"]', + '//div[@class="cikk-tartalom"]' + ), + 'strip' => array( + '//div[@class="cimkes-doboz"]', + '//div[@class="komment-wrap"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/imogenquest.net.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/imogenquest.net.php new file mode 100644 index 0000000..3214c62 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/imogenquest.net.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/index.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/index.hu.php new file mode 100644 index 0000000..727d7b7 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/index.hu.php @@ -0,0 +1,29 @@ + array( + '%.*%' => array( + 'test_url' => 'http://index.hu/mindekozben/poszt/2017/10/20/art_deco_budapest_varosnezo_zsebkonyv_bolla_zoltan/', + 'body' => array( + '//div[@class="mindenkozben_post_content content"]', + '//div[@id="content"]' + ), + 'strip' => array( + '//div[@class="topszponzor_wrapper"]', + '//ul[@class="cikk-cimkek"]', + '//div[@class="author-share-date-container"]', + '//div[@class="pp-list"]', + '//div[@class="social-stripe cikk-bottom-box"]', + '//div[@class="cikk-bottom-text-ad"]', + '//a[@name="hozzaszolasok"]', + '//div[@class="cikk-vegi-ajanlo-reklamok-container"]', + '//div[@id="comments"]', + '//div[@class="comments"]', + '//div[@class="linkpreview-box bekezdes_utan"]', + '//div[@class="lapozo"]', + '//div[@class="szelso-jobb"]', + '//div[@class="social cikk-bottom-box"]', + '//input' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/indiehaven.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/indiehaven.com.php new file mode 100644 index 0000000..a40ce69 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/indiehaven.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://indiehaven.com/no-mans-sky-is-a-solo-space-adventure-and-im-ok-with-that/', + 'body' => array( + '//section[contains(@class, "entry-content")]', + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/inforadio.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/inforadio.hu.php new file mode 100644 index 0000000..569013d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/inforadio.hu.php @@ -0,0 +1,25 @@ + array( + '%.*%' => array( + 'test_url' => 'http://inforadio.hu/belfold/2017/10/20/fontos_valtozas_vegleg_lezarnak_tobb_villamosatjarot_budapesten/', + 'body' => array( + '//div[@class="content-title"]', + '//div[@class="szelso-jobb-lead_container"]', + '//div[@class="cikk-torzs"]' + ), + 'strip' => array( + '//div[@id="microsite_microsite"]', + '//div[@class="cikk-bottom-text-ad"]', + '//div[@class="social-stripe_container"]', + '//div[@class="facebook-like-box"]', + '//div[@class="rovat sargabg rovatdobozcim"]', + '//div[@class="m-okosradio_magazin arenaMagazineItem"]', + '//header[@class="m-okosradio_header"]', + '//div[@class="m-okosradio_elo"]', + '//div[@class="m-okosradio_container"]', + '//form' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ing.dk.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ing.dk.php new file mode 100644 index 0000000..5a021a0 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ing.dk.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://ing.dk/artikel/smart-husisolering-og-styring-skal-mindske-japans-energikrise-164517', + 'body' => array( + '//section[contains(@class, "teaser")]', + '//section[contains(@class, "body")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/invisiblebread.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/invisiblebread.com.php new file mode 100644 index 0000000..90f8759 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/invisiblebread.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%()%' => '$1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ir.amd.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ir.amd.com.php new file mode 100644 index 0000000..af99fe9 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ir.amd.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'body' => array('//span[@class="ccbnTxt"]'), + 'strip' => array(), + 'test_url' => 'http://ir.amd.com/phoenix.zhtml?c=74093&p=RssLanding&cat=news&id=2055819', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantimes.co.jp.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantimes.co.jp.php new file mode 100644 index 0000000..9959441 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantimes.co.jp.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.japantimes.co.jp/news/2015/09/27/world/social-issues-world/pope-meets-sex-abuse-victims-philadelphia-promises-accountability/', + 'body' => array( + '//article[@role="main"]', + ), + 'strip' => array( + '//script', + '//header', + '//div[contains(@class, "meta")]', + '//div[@class="clearfix"]', + '//div[@class="OUTBRAIN"]', + '//ul[@id="content_footer_menu"]', + '//div[@class="article_footer_ad"]', + '//div[@id="disqus_thread"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantoday.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantoday.com.php new file mode 100644 index 0000000..22485d6 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/japantoday.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.japantoday.com/category/politics/view/japan-u-s-to-sign-new-base-environment-pact', + 'body' => array( + '//div[@id="article_container"]', + ), + 'strip' => array( + '//h2', + '//div[@id="article_info"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/journaldugeek.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/journaldugeek.com.php new file mode 100644 index 0000000..876b269 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/journaldugeek.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www./2014/05/20/le-playstation-now-arrive-en-beta-fermee-aux-etats-unis/', + 'body' => array( + '//div[@class="post-content"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/jsonline.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/jsonline.com.php new file mode 100644 index 0000000..5895256 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/jsonline.com.php @@ -0,0 +1,37 @@ + array( + '%.%/picture-gallery/%' => array( + 'test_url' => 'http://www.jsonline.com/picture-gallery/news/local/milwaukee/2017/02/22/photos-aclu-sues-milwaukee-police-over-profiling-stop-and-frisk/98250836/', + 'body' => array( + '//div[@class="priority-asset-gallery galleries standalone hasendslate"]', + ), + 'strip' => array( + '//div[@class="buy-photo-btn"]', + '//div[@class="gallery-thumbs thumbs pag-thumbs")]', + ), + ), + '%.*%' => array( + 'test_url' => 'http://www.jsonline.com/news/usandworld/as-many-as-a-million-expected-for-popes-last-mass-in-us-b99585180z1-329688131.html', + 'body' => array( + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//h1', + '//iframe', + '//span[@class="mycapture-small-btn mycapture-btn-with-text mycapture-expandable-photo-btn-small js-mycapture-btn-small"]', + '//div[@class="close-wrap"]', + '//div[contains(@class,"ui-video-wrapper")]', + '//div[contains(@class,"media-mob")]', + '//div[contains(@class,"left-mob")]', + '//div[contains(@class,"nerdbox")]', + '//p/span', + '//div[contains(@class,"oembed-asset")]', + '//*[contains(@class,"share")]', + '//div[contains(@class,"gallery-asset")]', + '//div[contains(@class,"oembed-asset")]', + '//div[@class="article-print-url"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/justcoolidea.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/justcoolidea.ru.php new file mode 100644 index 0000000..089ff29 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/justcoolidea.ru.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://justcoolidea.ru/idealnyj-sad-samodelnye-proekty-dlya-berezhlivogo-domovladeltsa/', + 'body' => array( + '//section[@class="entry-content"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[contains(@class, "essb_links")]', + '//*[contains(@rel, "nofollow")]', + '//*[contains(@class, "ads")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kanpai.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kanpai.fr.php new file mode 100644 index 0000000..c3a1abc --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kanpai.fr.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.kanpai.fr/japon/comment-donner-lheure-en-japonais.html', + 'body' => array( + '//div[@class="single-left"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/karriere.jobfinder.dk.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/karriere.jobfinder.dk.php new file mode 100644 index 0000000..25d6dfa --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/karriere.jobfinder.dk.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://karriere.jobfinder.dk/artikel/dansk-professor-skal-lede-smart-grid-forskning-20-millioner-dollars-763', + 'body' => array( + '//section[contains(@class, "teaser")]', + '//section[contains(@class, "body")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kisalfold.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kisalfold.hu.php new file mode 100644 index 0000000..7568901 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kisalfold.hu.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.kisalfold.hu/szorakozas/egy_15_eves_srac_szuntetheti_meg_a_wc-parat_budapesten/2536699/', + 'body' => array( + '//header[@class="single-article__header"]/h1', + '//header[@class="single-article__header"]/h2', + '//figure[@class="single-article__image"]/img', + '//div[@class="single-article__content"]/div[@id="single-article__lead"]', + '//div[@id="article_text"]' + ), + 'strip' => array( + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kiszamolo.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kiszamolo.hu.php new file mode 100644 index 0000000..c44a08f --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kiszamolo.hu.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'https://kiszamolo.hu/30-eve-volt-a-fekete-hetfo/', + 'body' => array( + '//article/h2', + '//article/div[@class="entry clearfix"]/p' + ), + 'strip' => array( + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kodi.tv.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kodi.tv.php new file mode 100644 index 0000000..439fc90 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/kodi.tv.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'https://kodi.tv/article/andwere-baaaaack', + 'body' => array( + '//div[@class="l-region--content"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreaherald.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreaherald.com.php new file mode 100644 index 0000000..9651056 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreaherald.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.koreaherald.com/view.php?ud=20150926000018', + 'body' => array( + '//div[@id="articleText"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreatimes.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreatimes.php new file mode 100644 index 0000000..f274b4a --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/koreatimes.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.koreatimes.co.kr/www/news/nation/2015/12/116_192409.html', + 'body' => array( + '//div[@id="p"]', + ), + 'strip' => array( + '//div[@id="webtalks_btn_listenDiv"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lastplacecomics.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lastplacecomics.com.php new file mode 100644 index 0000000..12697cc --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lastplacecomics.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/legorafi.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/legorafi.fr.php new file mode 100644 index 0000000..e6aae46 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/legorafi.fr.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => array( + 'http://www.legorafi.fr/2016/12/16/gorafi-magazine-bravo-vous-avez-bientot-presque-survecu-a-2016/', + 'http://www.legorafi.fr/2016/12/15/manuel-valls-promet-quune-fois-elu-il-debarrassera-la-france-de-manuel-valls/', + ), + 'body' => array( + '//section[@id="banner_magazine"]', + '//figure[@class="main_picture"]', + '//div[@class="content"]', + ), + 'strip' => array( + '//figcaption', + '//div[@class="sharebox"]', + '//div[@class="tags"]', + '//section[@class="taboola_article"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lejapon.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lejapon.fr.php new file mode 100644 index 0000000..8f2b293 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lejapon.fr.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lejapon.fr/guide-voyage-japon/5223/tokyo-sous-la-neige.htm', + 'body' => array( + '//div[@class="entry"]', + ), + 'strip' => array( + '//*[contains(@class, "addthis_toolbox")]', + '//*[contains(@class, "addthis_default_style")]', + '//*[@class="navigation small"]', + '//*[@id="related"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lesjoiesducode.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lesjoiesducode.fr.php new file mode 100644 index 0000000..369206a --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lesjoiesducode.fr.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lesjoiesducode.fr/post/75576211207/quand-lappli-ne-fonctionne-plus-sans-aucune-raison', + 'body' => array( + '//div[@class="blog-post-content"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lfg.co.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lfg.co.php new file mode 100644 index 0000000..d978a5f --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lfg.co.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.lfg.co/page/871/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+LookingForGroup+%28Looking+For+Group%29&utm_content=FeedBurner', + 'body' => array( + '//*[@id="comic"]/img | //*[@class="content"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.com.php new file mode 100644 index 0000000..b9a6933 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lifehacker.com/bring-water-bottle-caps-into-concerts-to-protect-your-d-1269334973', + 'body' => array( + '//div[contains(@class, "row")/img', + '//div[contains(@class, "content-column")]', + ), + 'strip' => array( + '//*[contains(@class, "meta")]', + '//span[contains(@class, "icon")]', + '//h1', + '//aside', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.ru.php new file mode 100644 index 0000000..bc140f6 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lifehacker.ru.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lifehacker.ru/2016/03/03/polymail/', + 'body' => array( + '//div[@class="post-content"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[@class="wp-thumbnail-caption"]', + '//*[contains(@class, "social-likes")]', + '//*[@class="jp-relatedposts"]', + '//*[contains(@class, "wpappbox")]', + '//*[contains(@class, "icon__image")]', + '//div[@id="hypercomments_widget"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux-magazin.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux-magazin.de.php new file mode 100644 index 0000000..f4bc07d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux-magazin.de.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.linux-magazin.de/Ausgaben/2017/09/AWS-Alternativen', + 'body' => array( + '//div[@class="attribute-content"]/div[@class="attribute-intro"]', + '(//div[@class="attribute-image"])[1]', + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//p[@class="attribute-advice"]', + ) + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.php new file mode 100644 index 0000000..2520d0d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.linux.org/threads/lua-the-scripting-interpreter.8352/', + 'body' => array( + '//div[@class="messageContent"]', + ), + 'strip' => array( + '//aside', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.ru.php new file mode 100644 index 0000000..7fa0249 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linux.org.ru.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.linux.org/threads/lua-the-scripting-interpreter.8352/', + 'body' => array( + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linuxinsider.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linuxinsider.com.php new file mode 100644 index 0000000..4e0a4cc --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/linuxinsider.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.linuxinsider.com/story/82526.html?rss=1', + 'body' => array( + '//div[@id="story"]', + ), + 'strip' => array( + '//script', + '//h1', + '//div[@id="story-toolbox1"]', + '//div[@id="story-byline"]', + '//div[@id="story"]/p', + '//div[@class="story-advertisement"]', + '//iframe', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lists.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lists.php new file mode 100644 index 0000000..c7051a2 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lists.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://lists.freebsd.org/pipermail/freebsd-announce/2013-September/001504.html', + 'body' => array( + '//pre', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loadingartist.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loadingartist.com.php new file mode 100644 index 0000000..d06ed12 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loadingartist.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loldwell.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loldwell.com.php new file mode 100644 index 0000000..d358e15 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/loldwell.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://loldwell.com/?comic=food-math-101', + 'body' => array('//*[@id="comic"]'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lukesurl.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lukesurl.com.php new file mode 100644 index 0000000..816233d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/lukesurl.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'body' => array('//div[@id="comic"]//img'), + 'strip' => array(), + 'test_url' => 'http://www.lukesurl.com/archives/comic/665-3-of-clubs', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/macg.co.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/macg.co.php new file mode 100644 index 0000000..bbe6dbc --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/macg.co.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.macg.co//logiciels/2014/05/feedly-sameliore-un-petit-peu-sur-mac-82205', + 'body' => array( + '//div[contains(@class, "field-name-body")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maclife.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maclife.de.php new file mode 100644 index 0000000..bca347e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maclife.de.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.maclife.de/news/neue-farbe-iphone-8-kommt-blush-gold-10094817.html', + 'body' => array( + '//div[contains(@class, "article_wrapper")]/p | //div[contains(@class, "article_wrapper")]/h2 | //div[@class="gallery"]//figure | //div[contains(@class, "gallery_single")]//figure', + ) + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/magyarkurir.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/magyarkurir.hu.php new file mode 100644 index 0000000..32d78bc --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/magyarkurir.hu.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.magyarkurir.hu/hirek/a-vilagszerte-ismert-dicsoito-csapat-hillsong-young-free-lep-fel-budapesten', + 'body' => array( + '//div[@class="behuzas"]' + ), + 'strip' => array( + '//div[@class="ikonsav"]', + '//p[@class="copyright"]', + '//div[@class="cimkek"]', + '//div[@id="footerbanner"]', + '//div[@class="rovat sargabg rovatdobozcim"]', + '//div[@class="rovatdoboz"]', + '//a[contains(., "Own")]', + '//a[@class="fblink"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marc.info.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marc.info.php new file mode 100644 index 0000000..5f582a6 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marc.info.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://marc.info/?l=openbsd-misc&m=141987113202061&w=2', + 'body' => array( + '//pre', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marriedtothesea.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marriedtothesea.com.php new file mode 100644 index 0000000..469640d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marriedtothesea.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.marriedtothesea.com/index.php?date=052915', + 'body' => array( + '//div[@align]/a/img', + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marycagle.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marycagle.com.php new file mode 100644 index 0000000..b8665e3 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/marycagle.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@id="cc-comic"]', + '//div[@class="cc-newsbody"]', + ), + 'strip' => array(), + 'test_url' => 'http://www.marycagle.com/letsspeakenglish/74-grim-reality/', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maximumble.thebookofbiff.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maximumble.thebookofbiff.com.php new file mode 100644 index 0000000..8880054 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/maximumble.thebookofbiff.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://maximumble.thebookofbiff.com/2015/04/20/1084-change/', + 'body' => array('//div[@id="comic"]/div/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/medium.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/medium.com.php new file mode 100644 index 0000000..e20860e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/medium.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'https://medium.com/lessons-learned/917b8b63ae3e', + 'body' => array( + '//div[@class="section-content"]', + ), + 'strip' => array( + '//div[contains(@class,"metabar")]', + '//img[contains(@class,"thumbnail")]', + '//h1', + '//blockquote', + '//div[@class="aspectRatioPlaceholder-fill"]', + '//footer' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mercworks.net.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mercworks.net.php new file mode 100644 index 0000000..c7a27de --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mercworks.net.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'body' => array('//div[@id="comic"]', + '//div[contains(@class,"entry-content")]', + ), + 'strip' => array(), + 'test_url' => 'http://mercworks.net/comicland/healthy-choice/', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/metronieuws.nl.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/metronieuws.nl.php new file mode 100644 index 0000000..5011169 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/metronieuws.nl.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.metronieuws.nl/sport/2015/04/broer-fellaini-zorgde-bijna-voor-paniek-bij-mourinho', + 'body' => array('//div[contains(@class,"article-top")]/div[contains(@class,"image-component")] | //div[@class="article-full-width"]/div[1]'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/milwaukeenns.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/milwaukeenns.php new file mode 100644 index 0000000..ddb29a5 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/milwaukeenns.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://milwaukeenns.org/2016/01/08/united-way-grant-enables-sdc-to-restore-free-tax-assistance-program/', + 'body' => array( + '//div[@class="pf-content"]', + ), + 'strip' => array( + '//div[@class="printfriendly"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mno.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mno.hu.php new file mode 100644 index 0000000..a2799b8 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mno.hu.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'https://mno.hu/kulfold/elnokot-valasztanak-szloveniaban-2422840', + 'body' => array( + '//div[@class="header"]/h1', + '//div[@class="content hircikk clearfix"]/p' + ), + 'strip' => array( + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mokepon.smackjeeves.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mokepon.smackjeeves.com.php new file mode 100644 index 0000000..1ddcd40 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mokepon.smackjeeves.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://mokepon.smackjeeves.com/comics/2120096/chapter-9-page-68/', + 'body' => array('//*[@id="comic_area_inner"]/img | //*[@id="comic_area_inner"]/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monandroid.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monandroid.com.php new file mode 100644 index 0000000..f87560e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monandroid.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.monandroid.com/blog/tutoriel-avance-activer-le-stockage-fusionne-sur-android-6-marshamallow-t12.html', + 'body' => array( + '//div[@class="blog-post-body"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monwindows.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monwindows.com.php new file mode 100644 index 0000000..b2b24d7 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/monwindows.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.monwindows.com/tout-savoir-sur-le-centre-d-action-de-windows-phone-8-1-t40574.html', + 'body' => array( + '//div[@class="blog-post-body"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/moya-planeta.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/moya-planeta.ru.php new file mode 100644 index 0000000..dd84284 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/moya-planeta.ru.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.moya-planeta.ru/travel/view/chto_yaponcu_horosho_russkomu_ne_ponyat_20432/', + 'body' => array( + '//div[@class="full_object"]', + ), + 'strip' => array( + '//div[@class="full_object_panel object_panel"]', + '//div[@class="full_object_panel_geo object_panel"]', + '//div[@class="full_object_title"]', + '//div[@class="full_object_social_likes"]', + '//div[@class="full_object_planeta_likes"]', + '//div[@class="full_object_go2comments"]', + '//div[@id="yandex_ad_R-163191-3"]', + '//div[@class="full_object_shop_article_recommend"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mrlovenstein.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mrlovenstein.com.php new file mode 100644 index 0000000..b971091 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mrlovenstein.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + '%alt="(.+)" */>%' => '/>
    $1', + '%\.png%' => '_rollover.png', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/muckrock.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/muckrock.com.php new file mode 100644 index 0000000..9e354a3 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/muckrock.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.muckrock.com/news/archives/2016/jan/13/5-concerns-private-prisons/', + 'body' => array( + '//div[@class="content"]', + ), + 'strip' => array( + '//div[@class="newsletter-widget"]', + '//div[@class="contributors"]', + '//time', + '//h1', + '//div[@class="secondary"]', + '//aside', + '//div[@class="articles__related"]' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mynorthshorenow.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mynorthshorenow.com.php new file mode 100644 index 0000000..b630915 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/mynorthshorenow.com.php @@ -0,0 +1,27 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.mynorthshorenow.com/story/news/local/fox-point/2017/04/04/fox-point-building-board-approves-dunwood-commons-project/99875570/', + 'body' => array( + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//h1', + '//iframe', + '//span[@class="mycapture-small-btn mycapture-btn-with-text mycapture-expandable-photo-btn-small js-mycapture-btn-small"]', + '//div[@class="close-wrap"]', + '//div[contains(@class,"ui-video-wrapper")]', + '//div[contains(@class,"media-mob")]', + '//div[contains(@class,"left-mob")]', + '//div[contains(@class,"nerdbox")]', + '//p/span', + '//div[contains(@class,"oembed-asset")]', + '//*[contains(@class,"share")]', + '//div[contains(@class,"gallery-asset")]', + '//div[contains(@class,"oembed-asset")]', + '//div[@class="article-print-url"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nakedCapitalism.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nakedCapitalism.php new file mode 100644 index 0000000..ec2d5fd --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nakedCapitalism.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://feedproxy.google.com/~r/NakedCapitalism/~3/JOBxEHxN8ZI/mark-blyth-liberalism-undermined-democracy-failure-democratic-party.html', + 'body' => array( + '//div[@class="pf-content"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nasa.gov.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nasa.gov.php new file mode 100644 index 0000000..c6692d0 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nasa.gov.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.nasa.gov/image-feature/jpl/pia20514/coy-dione', + 'body' => array( + '//div[@class="article-body"]', + ), + 'strip' => array( + '//div[@class="title-bar"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nat-geo.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nat-geo.ru.php new file mode 100644 index 0000000..1a42d99 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nat-geo.ru.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nat-geo.ru/fact/868093-knidos-antichnyy-naukograd/', + 'body' => array( + '//div[@class="article-inner-text"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nationaljournal.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nationaljournal.com.php new file mode 100644 index 0000000..5e612be --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nationaljournal.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nationaljournal.com/s/354962/south-carolina-evangelicals-outstrip-establishment?mref=home_top_main', + 'body' => array( + '//div[@class="section-body"]', + ), + 'strip' => array( + '//*[contains(@class, "-related")]', + '//*[contains(@class, "social")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nature.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nature.com.php new file mode 100644 index 0000000..6b9e87f --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nature.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nature.com/doifinder/10.1038/nature.2015.18340', + 'body' => array( + '//div[contains(@class,"main-content")]', + ), + 'strip' => array(), + ), + ), +); + diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nba.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nba.com.php new file mode 100644 index 0000000..c8ea926 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nba.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nba.com/2015/news/09/25/knicks-jackson-to-spend-more-time-around-coaching-staff.ap/index.html?rss=true', + 'body' => array( + '//div[@class="paragraphs"]', + ), + 'strip' => array( + '//div[@id="nbaArticleSocialWrapper_bot"]', + '//h5', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nedroid.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nedroid.com.php new file mode 100644 index 0000000..3214c62 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nedroid.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/networkworld.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/networkworld.com.php new file mode 100644 index 0000000..1852435 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/networkworld.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.networkworld.com/article/3020585/security/the-incident-response-fab-five.html', + 'body' => array( + '//figure/img[@class="hero-img"]', + '//section[@class="deck"]', + '//div[@itemprop="articleBody"] | //div[@itemprop="reviewBody"]', + '//div[@class="carousel-inside-crop"]', + ), + 'strip' => array( + '//script', + '//aside', + '//div[@class="credit"]', + '//div[@class="view-large"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/neustadt-ticker.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/neustadt-ticker.de.php new file mode 100644 index 0000000..e0c0d19 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/neustadt-ticker.de.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.neustadt-ticker.de/41302/alltag/kultur/demo-auf-der-boehmischen', + 'body' => array( + '//div[@class="entry-content"]', + ), + 'strip' => array( + '//*[contains(@class, "sharedaddy")]', + '//*[contains(@class, "yarpp-related")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nextinpact.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nextinpact.com.php new file mode 100644 index 0000000..29dd9d6 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nextinpact.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nextinpact.com/news/101122-3d-nand-intel-lance-six-nouvelles-gammes-ssd-pour-tous-usages.htm', + 'body' => array( + '//div[@class="container_article"]', + ), + 'strip' => array( + '//div[@class="infos_article"]', + '//div[@id="actu_auteur"]', + '//div[@id="soutenir_journaliste"]', + '//section[@id="bandeau_abonnez_vous"]', + '//br' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/niceteethcomic.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/niceteethcomic.com.php new file mode 100644 index 0000000..f41e443 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/niceteethcomic.com.php @@ -0,0 +1,10 @@ + array( + '%/archives.*%' => array( + 'test_url' => 'http://niceteethcomic.com/archives/page119/', + 'body' => array('//*[@class="comicpane"]/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nichtlustig.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nichtlustig.de.php new file mode 100644 index 0000000..4d083f9 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nichtlustig.de.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%.*static.nichtlustig.de/comics/full/(\\d+).*%s' => '', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nlcafe.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nlcafe.hu.php new file mode 100644 index 0000000..b85e6b2 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/nlcafe.hu.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.nlcafe.hu/ezvan/20171021/nyugdijas-drogdilert-fogtak-a-ferencvarosi-rendorok/', + 'body' => array( + '//div[@class="single-title"]', + '//div[@class="single-excerpt"]', + '//div[@class="single-post-container-content"]/p', + '//div[@class="single-post-container-content"]/div' + ), + 'strip' => array( + '//div[@class="widget-container related-articles bigdata-widget related-full"]', + '//div[@class="banner-container clear-banner-row clearfix"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/novo-argumente.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/novo-argumente.com.php new file mode 100644 index 0000000..cef3595 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/novo-argumente.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.novo-argumente.com/artikel/der_kampf_gegen_die_schlafkrankheit', + 'body' => array( + '//main/div/article', + ), + 'strip' => array( + '//*[@class="artikel-datum"]', + '//*[@class="artikel-titel"]', + '//*[@class="artikel-autor"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/oglaf.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/oglaf.com.php new file mode 100644 index 0000000..8b2b5b6 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/oglaf.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@id="strip"]', + '//a/div[@id="nx"]/..', + ), + 'strip' => array(), + 'test_url' => 'http://oglaf.com/slodging/', + ), + ), + 'filter' => array( + '%.*%' => array( + '%alt="(.+)" title="(.+)" */>%' => '/>
    $1
    $2
    ', + '%%' => 'Next page', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onhax.net.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onhax.net.php new file mode 100644 index 0000000..213849d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onhax.net.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://onhax.net/process-lasso-8-9-1-4-pro-key-portable-is-here-latest', + 'body' => array( + '//div[@class="postcontent"]', + ), + 'strip' => array( + '//*[@class="sharedaddy sd-sharing-enabled"]', + '//*[@class="yarpp-related"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onlinekosten.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onlinekosten.de.php new file mode 100644 index 0000000..7382612 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onlinekosten.de.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.onlinekosten.de/news/android-8-0-die-neuen-features-im-ueberblick_209619.html?utm_source=rss&utm_medium=feed&utm_campaign=android-8-0-die-neuen-features-im-ueberblick', + 'body' => array( + '//p[@class="cms-widget_article_lead"]', + '//img[@class="bec_img"]', + '//div[@class="cms-widget_article_body"]', + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onmilwaukee.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onmilwaukee.php new file mode 100644 index 0000000..f66ac4b --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/onmilwaukee.php @@ -0,0 +1,24 @@ + array( + '%.*%' => array( + 'test_url' => 'http://onmilwaukee.com/movies/articles/downerspelunking.html', + 'body' => array( + '//article[contains(@class, "show")]', + ), + 'strip' => array( + '//h1', + '//div[contains(@class,"-ad")]', + '//div[contains(@class,"_ad")]', + '//div[@id="pub_wrapper"]', + '//div[contains(@class,"share_tools")]', + '//div[@class="clearfix"]', + '//div[contains(@class,"image_control")]', + '//section[@class="ribboned"]', + '//div[contains(@class,"sidebar")]', + '//aside[@class="article_tag_list"]', + '//section[contains(@id,"more_posts")]' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openculture.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openculture.com.php new file mode 100644 index 0000000..84f2bee --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openculture.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.openculture.com/2017/03/are-we-living-inside-a-computer-simulation-watch-the-simulation-argument.html', + 'body' => array( + '//div[@class="entry"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opennet.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opennet.ru.php new file mode 100644 index 0000000..1fb7722 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opennet.ru.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.opennet.ru/opennews/art.shtml?num=46549', + 'body' => array( + '//*[@id="r_memo"]', + ), + 'strip' => array( + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openrightsgroup.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openrightsgroup.org.php new file mode 100644 index 0000000..94139a7 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/openrightsgroup.org.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.openrightsgroup.org/blog/2014/3-days-to-go-till-orgcon2014', + 'body' => array( + '//div[contains(@class, "content")]/div', + ), + 'strip' => array( + '//h2[1]', + '//div[@class="info"]', + '//div[@class="tags"]', + '//div[@class="comments"]', + '//div[@class="breadcrumbs"]', + '//h1[@class="pageTitle"]', + '//p[@class="bookmarkThis"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opensource.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opensource.com.php new file mode 100644 index 0000000..60f3577 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/opensource.com.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://opensource.com/life/15/10/how-internet-things-will-change-way-we-think', + 'body' => array( + '//div[@id="article-template"]', + ), + 'strip' => array( + '//div[contains(@class,"os-article__sidebar")]', + '//div[@class="panel-pane pane-node-title"]', + '//div[@class="panel-pane pane-os-article-byline"]', + '//ul', + '//div[contains(@class,"-license")]', + '//div[contains(@class,"-tags")]', + '//div[@class="panel-pane pane-os-article-byline"]', + '//div[@class="os-article__content-below"]', + '//div[@id="comments"]' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/optipess.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/optipess.com.php new file mode 100644 index 0000000..3214c62 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/optipess.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/origo.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/origo.hu.php new file mode 100644 index 0000000..7bd9496 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/origo.hu.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.origo.hu/itthon/20171019-hamisan-tanuskodott-az-ugyved-ezert-nem-praktizalhat.html', + 'body' => array( + '//header[@id="article-head"]/h1', + '//article[@id="article-center"]' + ), + 'strip' => array( + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/osnews.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/osnews.com.php new file mode 100644 index 0000000..1d1396c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/osnews.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://osnews.com/story/28863/Google_said_to_be_under_US_antitrust_scrutiny_over_Android', + 'body' => array( + '//div[@class="newscontent1"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pastebin.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pastebin.com.php new file mode 100644 index 0000000..b20bf41 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pastebin.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://pastebin.com/ed1pP9Ak', + 'body' => array( + '//div[@class="text"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pcgameshardware.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pcgameshardware.de.php new file mode 100644 index 0000000..f180aee --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pcgameshardware.de.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.pcgameshardware.de/Dragon-Age-Thema-259929/News/Plaene-fuer-Teil-4-und-5-der-Serie-1235682/', + 'body' => array( + '//p[@class="introText"]', + '//figure[contains(@class, "articleBigTeaser")]', + '//div[@id="articleTextBody"]//p | //div[@id="articleTextBody"]//h2[@class="anchorHeadline"]', + ), + 'strip' => array( + '//p[@class="introText"]//time', + ) + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/peebleslab.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/peebleslab.com.php new file mode 100644 index 0000000..ce4891d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/peebleslab.com.php @@ -0,0 +1,9 @@ + array( + '%.*%' => array( + // the extra space is required to strip the title cleanly + '%title="(.+) " */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/penny-arcade.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/penny-arcade.com.php new file mode 100644 index 0000000..dd39983 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/penny-arcade.com.php @@ -0,0 +1,21 @@ + array( + '%/news/.*%' => array( + 'test_url' => 'http://penny-arcade.com/news/post/2015/04/15/101-part-two', + 'body' => array( + '//*[@class="postBody"]/*', + ), + 'strip' => array( + ), + ), + '%/comic/.*%' => array( + 'test_url' => 'http://penny-arcade.com/comic/2015/04/15', + 'body' => array( + '//*[@id="comicFrame"]/a/img', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pixelbeat.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pixelbeat.org.php new file mode 100644 index 0000000..fa9052e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pixelbeat.org.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.pixelbeat.org/programming/sigpipe_handling.html#1425573246', + 'body' => array( + '//div[@class="contentText"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/plus.google.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/plus.google.com.php new file mode 100644 index 0000000..5e48a6c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/plus.google.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'https://plus.google.com/+LarryPage/posts/Lh8SKC6sED1', + 'body' => array( + '//div[@role="article"]/div[contains(@class, "eE")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/popstrip.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/popstrip.com.php new file mode 100644 index 0000000..801a281 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/popstrip.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%( '$1$2$1bonus.png"/>', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/portfolio.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/portfolio.hu.php new file mode 100644 index 0000000..ad01dad --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/portfolio.hu.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.portfolio.hu/gazdasag/mennybe-vagy-pokolba-megy-ma-a-cseh-trump.265833.html', + 'body' => array( + '//div[@id="cikk"]/h1', + '//div[@class="smscontent"]' + ), + 'strip' => array( + '//div[@class="traderhirdetes ga_viewanalytics"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pro-linux.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pro-linux.de.php new file mode 100644 index 0000000..bc76630 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/pro-linux.de.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.pro-linux.de/news/1/25252/chrome-62-erschienen.html', + 'body' => array( + '//div[@id="news"]', + ), + 'strip' => array( + '//h3[@class="topic"]', + '//h2[@class="title"]', + '//div[@class="picto"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publicpolicyforum.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publicpolicyforum.org.php new file mode 100644 index 0000000..5dc8be8 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publicpolicyforum.org.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'https://publicpolicyforum.org/blog/going-extra-mile', + 'body' => array( + '//div[contains(@class,"field-name-post-date")]', + '//div[contains(@class,"field-name-body")]', + ), + 'strip' => array( + '//img[@src="http://publicpolicyforum.org/sites/default/files/logo3.jpg"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publy.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publy.ru.php new file mode 100644 index 0000000..bcfeeb9 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/publy.ru.php @@ -0,0 +1,24 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.publy.ru/post/19988', + 'body' => array( + '//div[@class="singlepost"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[@class="featured"]', + '//*[@class="toc_white no_bullets"]', + '//*[@class="toc_title"]', + '//*[@class="pba"]', + '//*[@class="comments"]', + '//*[contains(@class, "g-single")]', + '//*[@class="ts-fab-wrapper"]', + '//*[contains(@class, "wp_rp_wrap")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/putaindecode.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/putaindecode.fr.php new file mode 100644 index 0000000..9fa5568 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/putaindecode.fr.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://putaindecode.fr/posts/js/etat-lieux-js-modulaire-front/', + 'body' => array( + '//*[@class="putainde-Post-md"]', + ), + 'strip' => array( + '//*[contains(@class, "inlineimg")]', + '//*[contains(@class, "comment-respond")]', + '//header', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/recode.net.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/recode.net.php new file mode 100644 index 0000000..343cd12 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/recode.net.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://recode.net/2015/09/26/big-tech-rolls-out-red-carpet-for-indian-prime-minister-lobbies-behind-closed-doors/', + 'body' => array( + '//img[contains(@class,"attachment-large")]', + '//div[contains(@class,"postarea")]', + '//li[@class,"author"]', + ), + 'strip' => array( + '//script', + '//div[contains(@class,"sharedaddy")]', + '//div[@class="post-send-off"]', + '//div[@class="large-12 columns"]', + '//div[contains(@class,"inner-related-article")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/retractionwatch.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/retractionwatch.com.php new file mode 100644 index 0000000..b97c73e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/retractionwatch.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://retractionwatch.com/2015/11/12/psychologist-jens-forster-settles-case-by-agreeing-to-2-retractions/', + 'body' => array( + '//*[@class="main"]', + '//*[@class="entry-content"]', + ), + 'strip' => array( + '//*[contains(@class, "sharedaddy")]', + '//*[contains(@class, "jp-relatedposts")]', + '//p[@class="p1"]', + ) + ) + ) +); + diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rockpapershotgun.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rockpapershotgun.com.php new file mode 100644 index 0000000..7111078 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rockpapershotgun.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.rockpapershotgun.com/2016/08/26/the-divisions-expansions-delayed-to-improve-the-game/', + 'body' => array( + '//div[@class="entry"]', + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rue89.nouvelobs.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rue89.nouvelobs.com.php new file mode 100644 index 0000000..cb9116a --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rue89.nouvelobs.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://rue89.feedsportal.com/c/33822/f/608948/s/30999fa0/sc/24/l/0L0Srue890N0C20A130C0A80C30A0Cfaisait0Eboris0Eboillon0Eex0Esarko0Eboy0E350A0E0A0A0A0Eeuros0Egare0Enord0E245315/story01.htm', + 'body' => array( + '//*[@id="article"]/div[contains(@class, "content")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rugbyrama.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rugbyrama.fr.php new file mode 100644 index 0000000..9915c23 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/rugbyrama.fr.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.rugbyrama.fr/rugby/top-14/2015-2016/top-14-hayman-coupe-du-monde-finale-2012-lutte.-voici-levan-chilachava-toulon_sto5283863/story.shtml', + 'body' => array( + '//div[@class="storyfull__content"]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[@class="share-buttons"]', + '//*[@class="ad"]', + '//*[@class="hide-desktop"]', + '//*[@id="tracking_img"]', + ) + ) + ) +); \ No newline at end of file diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/salonkolumnisten.com b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/salonkolumnisten.com new file mode 100644 index 0000000..37f43e9 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/salonkolumnisten.com @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.salonkolumnisten.com/schulpolitik-niedersachsen/', + 'body' => array( + '//div[@id="main"]/div[contains(@class, "featimg")]', + '//div[@id="main"]/article/div[contains(@class, "entry-content")]', + ), + 'strip' => array( + '//div[@id="main"]/article/div[contains(@class, "entry-content")]/a[1]', + '//div[@id="main"]/article/div[contains(@class, "entry-content")]/a[1]', + ), + ), + ), +); + diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/satwcomic.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/satwcomic.com.php new file mode 100644 index 0000000..d63fc11 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/satwcomic.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://satwcomic.com/day-at-the-beach', + 'body' => array( + '//div[@class="container"]/center/a/img', + '//span[@itemprop="articleBody"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/science-skeptical.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/science-skeptical.de.php new file mode 100644 index 0000000..fcf045f --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/science-skeptical.de.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.science-skeptical.de/politik/diesel-die-lueckenmedien-im-glashaus-6/0016080/', + 'body' => array( + '//div[@class="pf-content"]', + ), + 'strip' => array( + '//div[contains(@class, "printfriendly")]', + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/scrumalliance.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/scrumalliance.org.php new file mode 100644 index 0000000..7835fd9 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/scrumalliance.org.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.scrumalliance.org/community/articles/2015/march/an-introduction-to-agile-project-intake?feed=articles', + 'body' => array( + '//div[@class="article_content"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/securityfocus.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/securityfocus.com.php new file mode 100644 index 0000000..0104514 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/securityfocus.com.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.securityfocus.com/archive/1/540139', + 'body' => array( + '//div[@id="vulnerability"]', + '//div[@class="comments_reply"]', + ), + 'strip' => array( + '//span[@class="title"]', + '//div[@id="logo_new"]', + '//div[@id="bannerAd"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sentfromthemoon.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sentfromthemoon.com.php new file mode 100644 index 0000000..f435417 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sentfromthemoon.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'body' => array( + '//div[@class="comicpane"]/a/img', + '//div[@class="entry"]', + ), + 'strip' => array(), + 'test_url' => 'http://sentfromthemoon.com/archives/1417', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sitepoint.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sitepoint.com.php new file mode 100644 index 0000000..ab0eb7d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sitepoint.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.sitepoint.com/creating-hello-world-app-swift/', + 'body' => array( + '//section[@class="article_body"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/slashdot.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/slashdot.org.php new file mode 100644 index 0000000..89ced8b --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/slashdot.org.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://science.slashdot.org/story/15/04/20/0528253/pull-top-can-tabs-at-50-reach-historic-archaeological-status', + 'body' => array( + '//article/div[@class="body"] | //article[@class="layout-article"]/div[@class="elips"]', ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smallhousebliss.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smallhousebliss.com.php new file mode 100644 index 0000000..8c13c44 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smallhousebliss.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://smallhousebliss.com/2013/08/29/house-g-by-lode-architecture/', + 'body' => array( + '//div[@class="post-content"]', + ), + 'strip' => array( + '//*[contains(@class, "gallery")]', + '//*[contains(@class, "share")]', + '//*[contains(@class, "wpcnt")]', + '//*[contains(@class, "meta")]', + '//*[contains(@class, "postitle")]', + '//*[@id="nav-below"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smarthomewelt.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smarthomewelt.de.php new file mode 100644 index 0000000..7463abc --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smarthomewelt.de.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://smarthomewelt.de/apple-tv-amazon-echo-smart-home/', + 'body' => array('//div[@class="entry-inner"]/p | //div[@class="entry-inner"]/div[contains(@class,"wp-caption")]'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smashingmagazine.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smashingmagazine.com.php new file mode 100644 index 0000000..cbe1072 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smashingmagazine.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.smashingmagazine.com/2015/04/17/using-sketch-for-responsive-web-design-case-study/', + 'body' => array('//article[contains(@class,"post")]/p'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smbc-comics.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smbc-comics.com.php new file mode 100644 index 0000000..42262dc --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/smbc-comics.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.smbc-comics.com/comic/the-troll-toll', + 'body' => array( + '//div[@id="cc-comicbody"]', + '//div[@id="aftercomic"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/snopes.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/snopes.com.php new file mode 100644 index 0000000..b0fe655 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/snopes.com.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.snopes.com/bacca-brides-on-tour/', + 'body' => array( + '//article', + ), + 'strip' => array( + '//span[@itemprop="author"]', + '//div[contains(@class,"author-")]', + '//h1', + '//style', + '//div[contains(@class,"socialShares")]', + '//div[contains(@class,"ad-unit")]', + '//aside', + '//div[contains(@class,"boomtrain")]', + '//footer' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/soundandvision.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/soundandvision.com.php new file mode 100644 index 0000000..6448bb0 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/soundandvision.com.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.soundandvision.com/content/james-guthrie-mixing-roger-waters-and-pink-floyd-51', + 'body' => array( + '//div[@id="left"]', + ), + 'strip' => array( + '//div[@class="meta"]', + '//div[@class="ratingsbox"]', + '//h1', + '//h2', + '//addthis', + '//comment-links', + '//div[@class="book-navigation"]', + '//div[@class="comment-links"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/spiegel.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/spiegel.de.php new file mode 100644 index 0000000..b42c3aa --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/spiegel.de.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.spiegel.de/politik/ausland/afrika-angola-geht-gegen-islam-vor-und-schliesst-moscheen-a-935788.html', + 'body' => array( + '//div[@class="spArticleContent"]/p | //div[@class="spArticleContent"]//img[@class="spResponsiveImage "]', + ), + 'strip' => array( + '//div[@class="author-details"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stereophile.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stereophile.com.php new file mode 100644 index 0000000..8e410be --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stereophile.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.stereophile.com/content/2015-rocky-mountain-audio-fest-starts-friday', + 'body' => array( + '//div[@class="content clear-block"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stupidfox.net.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stupidfox.net.php new file mode 100644 index 0000000..61182d7 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/stupidfox.net.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://stupidfox.net/134-sleepy-time', + 'body' => array( + '//div[@class="comicmid"]/center/a/img', + '//div[@class="stand_high"]', + ), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/subtraction.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/subtraction.com.php new file mode 100644 index 0000000..6d74427 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/subtraction.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.subtraction.com/2015/06/06/time-lapse-video-of-one-world-trade-center/', + 'body' => array('//article/div[@class="entry-content"]'), + 'strip' => array(), + ), + ), + 'filter' => array( + '%.*%' => array( + '%\+%' => '', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sz.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sz.de.php new file mode 100644 index 0000000..90bde5a --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/sz.de.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://sz.de/1.2443161', + 'body' => array('//article[@id="sitecontent"]/section[@class="topenrichment"]//img | //article[@id="sitecontent"]/section[@class="body"]/section[@class="authors"]/preceding-sibling::*[not(contains(@class, "ad"))]'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/takprosto.cc.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/takprosto.cc.php new file mode 100644 index 0000000..624ef90 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/takprosto.cc.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://takprosto.cc/kokteyl-dlya-pohudeniya-v-domashnih-usloviyah/', + 'body' => array( + '//div[contains(@class, "entry-contentt")]', + ), + 'strip' => array( + '//script', + '//form', + '//style', + '//*[@class="views_post"]', + '//*[contains(@class, "mailchimp-box")]', + '//*[contains(@class, "essb_links")]', + '//*[contains(@rel, "nofollow")]', + '//*[contains(@class, "ads")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/techcrunch.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/techcrunch.com.php new file mode 100644 index 0000000..230b791 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/techcrunch.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://techcrunch.com/2013/08/31/indias-visa-maze/', + 'body' => array( + '//div[contains(@class, "media-container")]', + '//div[contains(@class, "article-entry")]', + ), + 'strip' => array( + '//*[contains(@class, "module-crunchbase")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/the-ebook-reader.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/the-ebook-reader.com.php new file mode 100644 index 0000000..3b01eb9 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/the-ebook-reader.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://blog.the-ebook-reader.com/2015/09/25/kobo-glo-hd-and-kobo-touch-2-0-covers-and-cases-roundup/', + 'body' => array( + '//div[@class="entry"]', + ), + 'strip' => array( + '//div[@id="share"]', + '//div[contains(@class,"ItemCenter")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theatlantic.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theatlantic.com.php new file mode 100644 index 0000000..bfad4ab --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theatlantic.com.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.theatlantic.com/politics/archive/2015/09/what-does-it-mean-to-lament-the-poor-inside-panem/407317/', + 'body' => array( + '//picture[@class="img"]', + '//figure/figcaption/span', + '//div/p[@itemprop="description"]', + '//div[@class="article-body"]', + '//ul[@class="photos"]', + ), + 'strip' => array( + '//aside[@class="callout"]', + '//span[@class="credit"]', + '//figcaption[@class="credit"]', + '//aside[contains(@class,"partner-box")]', + '//div[contains(@class,"ad")]', + '//a[contains(@class,"social-icon")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theawkwardyeti.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theawkwardyeti.com.php new file mode 100644 index 0000000..fd4f3d5 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theawkwardyeti.com.php @@ -0,0 +1,12 @@ + array( + '%/comic/.*%' => array( + 'test_url' => 'http://theawkwardyeti.com/comic/things-to-do/', + 'body' => array( + '//div[@id="comic"]' + ), + 'strip' => array() + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thecodinglove.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thecodinglove.com.php new file mode 100644 index 0000000..c6ec5bf --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thecodinglove.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://thecodinglove.com/post/116897934767', + 'body' => array('//div[@class="bodytype"]'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thedoghousediaries.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thedoghousediaries.com.php new file mode 100644 index 0000000..d2f840d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thedoghousediaries.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'body' => array( + '//div[@class="comicpane"]/a/img', + '//div[@class="entry"]', + ), + 'strip' => array(), + 'test_url' => 'http://thedoghousediaries.com/6023', + ), + ), + 'filter' => array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thegamercat.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thegamercat.com.php new file mode 100644 index 0000000..f9b4637 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thegamercat.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.thegamercat.com/comic/just-no/', + 'body' => array('//div[@id="comic"] | //div[@class="post-content"]/div[@class="entry"]/p'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thehindu.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thehindu.com.php new file mode 100644 index 0000000..1e6735b --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thehindu.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.thehindu.com/sci-tech/science/why-is-the-shape-of-cells-in-a-honeycomb-always-hexagonal/article7692306.ece?utm_source=RSS_Feed&utm_medium=RSS&utm_campaign=RSS_Syndication', + 'body' => array( + '//div/img[@class="main-image"]', + '//div[@class="photo-caption"]', + '//div[@class="articleLead"]', + '//p', + '//span[@class="upper"]', + ), + 'strip' => array( + '//div[@id="articleKeywords"]', + '//div[@class="photo-source"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thelocal.se.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thelocal.se.php new file mode 100644 index 0000000..c3ec250 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thelocal.se.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'www.thelocal.se/20161219/this-swede-can-memorize-hundreds-of-numbers-in-only-five-minutes', + 'body' => array( + '//div[@id="article-photo"]', + '//div[@id="article-description"]', + '//div[@id="article-body"]', + ), + 'strip' => array( + '//div[@id="article-info-middle"]', + ) + ) + ) +); + diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themerepublic.net.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themerepublic.net.php new file mode 100644 index 0000000..bc47b27 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themerepublic.net.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.themerepublic.net/2015/04/david-lopez-pitoko.html?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+blogspot%2FDngUJ+%28Theme+Republic%29&utm_content=FeedBurner', + 'body' => array('//*[@class="post-body"]'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themoscowtimes.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themoscowtimes.com.php new file mode 100644 index 0000000..0f5bf75 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/themoscowtimes.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.themoscowtimes.com/business/article/535500.html', + 'body' => array( + '//div[@class="article_main_img"]', + '//div[@class="article_text"]', + ), + 'strip' => array( + '//div[@class="articlebottom"]', + '//p/b', + '//p/a[contains(@href, "/article.php?id=")]', + '//div[@class="disqus_wrap"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thenewslens.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thenewslens.com.php new file mode 100644 index 0000000..7538170 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thenewslens.com.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://international.thenewslens.com/post/255032/', + 'body' => array( + '//div[@class="article-section"]', + ), + 'strip' => array( + '//div[contains(@class,"ad-")]', + '//div[@class="article-title-box"]', + '//div[@class="function-box"]', + '//p/span', + '//aside', + '//footer', + '//div[@class="article-infoBot-box"]', + '//div[contains(@class,"standard-container")]' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theodd1sout.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theodd1sout.com.php new file mode 100644 index 0000000..d06ed12 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theodd1sout.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%-150x150%' => '', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theonion.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theonion.com.php new file mode 100644 index 0000000..acbfd36 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theonion.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.theonion.com/article/wild-eyed-jim-harbaugh-informs-players-they-must-k-51397?utm_medium=RSS&utm_campaign=feeds', + 'body' => array( + '//div[@class="content-masthead"]/figure/div/noscript/img', + '//div[@class="content-text"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theregister.co.uk.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theregister.co.uk.php new file mode 100644 index 0000000..896365a --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theregister.co.uk.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.theregister.co.uk/2017/10/21/purism_cleanses_laptops_of_intel_management_engine/', + 'body' => array( + '//div[@id="article"]', + ), + 'strip' => array( + '//div[@class="byline_and_share"]', + '//div[@class="social_btns alt_colour dcl"]', + '//div[@class="promo_article"]', + '//div[@id="article_body_btm"]', + '//p[@class="wptl btm"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thestandard.com.hk.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thestandard.com.hk.php new file mode 100644 index 0000000..1163b34 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/thestandard.com.hk.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.thestandard.com.hk/breaking_news_detail.asp?id=67156', + 'body' => array( + '//table/tr/td/span[@class="bodyCopy"]', + ), + 'strip' => array( + '//script', + '//br', + '//map[@name="gif_bar"]', + '//img[contains(@usemap,"gif_bar")]', + '//a', + '//span[@class="bodyHeadline"]', + '//i', + '//b', + '//table', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theverge.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theverge.com.php new file mode 100644 index 0000000..09e876d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/theverge.com.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.theverge.com/2017/11/11/16624298/mindhunter-netflix-show-david-fincher-review', + 'body' => array( + '//figure[@class="e-image e-image--hero"]/span[@class="e-image__inner"]', + '//div[@class="c-entry-content"]', + ), + 'strip' => array( + '//div[@class="c-related-list"]', + '//div[@class="c-page-title"]', + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/threepanelsoul.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/threepanelsoul.com.php new file mode 100644 index 0000000..4af6196 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/threepanelsoul.com.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'body' => array( + '//img[@id="cc-comic"]', + ), + 'test_url' => 'http://www.threepanelsoul.com/comic/uncloaking', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tichyseinblick.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tichyseinblick.de.php new file mode 100644 index 0000000..6fba3df --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tichyseinblick.de.php @@ -0,0 +1,22 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.tichyseinblick.de/daili-es-sentials/jamaika-reaktionen-der-enttaeuschten/', + 'body' => array( + '//article' + ), + 'strip' => array( + '//header', + '//footer', + '//div[@class="mod-cad2"]', + '//ul[contains(@class, "social")]', + '//div[@class="rty-pop-up"]', + '//div[@class="pagelink"]', + '//div[@id="reward"]', + '//div[@class="rty-block-plista"]' + ) + ), + ), +); + diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/timesofindia.indiatimes.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/timesofindia.indiatimes.com.php new file mode 100644 index 0000000..1924680 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/timesofindia.indiatimes.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://timesofindia.indiatimes.com/city/mangaluru/Adani-UPCL-to-release-CSR-grant-of-Rs-3-74-crore-to-YellurGram-Panchayat/articleshow/50512116.cms', + 'body' => array( + '//div[@class="article_content clearfix"]', + '//section[@class="highlight clearfix"]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/totalcar.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/totalcar.hu.php new file mode 100644 index 0000000..f2e009d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/totalcar.hu.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://totalcar.hu/tesztek/2017/10/21/veteran_fiat-abarth_1000tc_1968/', + 'body' => array( + '//div[@class="content-title"]', + '//div[@class="lead-container"]/div[@class="lead"]', + '//div[@class="cikk-torzs"]' + ), + 'strip' => array( + '//span[@class="gallery_title newline"]', + '//div[@class="social-stripe cikk-bottom-box"]', + '//div[@class="cikk-bottom-text-ad"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tozsdeforum.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tozsdeforum.hu.php new file mode 100644 index 0000000..97a0da0 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tozsdeforum.hu.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.tozsdeforum.hu/szemelyes-penzugyek/napi-penzugyek/ezek-a-legnepszerubb-turistacelpontok-voltal-mar-mindenhol-87181.html', + 'body' => array( + '//header/h1', + '//div[@class="title_img"]', + '//article/div[@class="tf-post"]/div[@class="p"]/p|//article/div[@class="tf-post"]/div[@class="p"]/h3|//article/div[@class="tf-post"]/div[@class="p"]/blockquote' + ), + 'strip' => array( + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travel-dealz.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travel-dealz.de.php new file mode 100644 index 0000000..4ee4fcd --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travel-dealz.de.php @@ -0,0 +1,15 @@ + array( + '%^/blog.*%' => array( + 'test_url' => 'http://travel-dealz.de/blog/venere-gutschein/', + 'body' => array('//div[@class="post-entry"]'), + 'strip' => array( + '//*[@id="jp-relatedposts"]', + '//*[@class="post-meta"]', + '//*[@class="post-data"]', + '//*[@id="author-meta"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travelo.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travelo.hu.php new file mode 100644 index 0000000..3ab8ca4 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/travelo.hu.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://travelo.hu/csaladbarat/2017/10/20/mar_csak_egy_het_es_kezdodik_az_oszi_szunet/', + 'body' => array( + '//div[@id="content"]/h1', + '//div[@id="kopf"]', + '//div[@id="szoveg"]' + ), + 'strip' => array( + '//div[@class="goAdverticum"]', + '//h1[@class="border"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treehugger.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treehugger.com.php new file mode 100644 index 0000000..55eb7e0 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treehugger.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.treehugger.com/uncategorized/top-ten-posts-week-bunnies-2.html', + 'body' => array( + '//div[contains(@class, "promo-image")]', + '//div[contains(@id, "entry-body")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treelobsters.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treelobsters.com.php new file mode 100644 index 0000000..3214c62 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/treelobsters.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%title="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tutorialzine.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tutorialzine.com.php new file mode 100644 index 0000000..7e39145 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/tutorialzine.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'https://tutorialzine.com/2017/10/15-interesting-javascript-and-css-libraries-for-october-2017', + 'body' => array( + '//article' + ), + 'strip' => array( + '//div[@class="article__header"]', + '//div[@class="article__share"]', + '//div[@class="article__footer"]', + '//div[@id="article__related-articles"]', + '//div[@class="webappstudio-animation"]', + '//div[@class="ad-container adsbygoogle hidden-xs hidden-sm"]', + '//script' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twogag.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twogag.com.php new file mode 100644 index 0000000..79f4f62 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twogag.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%http://www.twogag.com/comics-rss/([^.]+)\\.jpg%' => 'http://www.twogag.com/comics/$1.jpg', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twokinds.keenspot.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twokinds.keenspot.com.php new file mode 100644 index 0000000..3428fcb --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/twokinds.keenspot.com.php @@ -0,0 +1,10 @@ + array( + '%.*%' => array( + 'test_url' => 'http://twokinds.keenspot.com/archive.php?p=0', + 'body' => array('//*[@class="comic"]/div/a/img | //*[@class="comic"]/div/img | //*[@id="cg_img"]/img | //*[@id="cg_img"]/a/img'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/undeadly.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/undeadly.org.php new file mode 100644 index 0000000..8b15900 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/undeadly.org.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://undeadly.org/cgi?action=article&sid=20141101181155', + 'body' => array( + '/html/body/table[3]/tbody/tr/td[1]/table[2]/tr/td[1]', + ), + 'strip' => array( + '//font', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/upi.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/upi.com.php new file mode 100644 index 0000000..ec8d1a1 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/upi.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.upi.com/Top_News/US/2015/09/26/Tech-giants-Hollywood-stars-among-guests-at-state-dinner-for-Chinas-Xi-Jinping/4541443281006/', + 'body' => array( + '//div[@class="img"]', + '//div/article[@itemprop="articleBody"]', + ), + 'strip' => array( + '//div[@align="center"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/usatoday.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/usatoday.com.php new file mode 100644 index 0000000..edd6aa4 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/usatoday.com.php @@ -0,0 +1,27 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.usatoday.com/story/life/music/2017/02/13/things-you-should-know-happened-grammy-awards-2017/97833734/', + 'body' => array( + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//script', + '//h1', + '//iframe', + '//span[@class="mycapture-small-btn mycapture-btn-with-text mycapture-expandable-photo-btn-small js-mycapture-btn-small"]', + '//div[@class="close-wrap"]', + '//div[contains(@class,"ui-video-wrapper")]', + '//div[contains(@class,"media-mob")]', + '//div[contains(@class,"left-mob")]', + '//div[contains(@class,"nerdbox")]', + '//div[contains(@class,"oembed-asset")]', + '//*[contains(@class,"share")]', + '//div[contains(@class,"gallery-asset")]', + '//div[contains(@class,"oembed-asset")]', + '//div[@class="article-print-url"]' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/version2.dk.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/version2.dk.php new file mode 100644 index 0000000..a6d49f2 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/version2.dk.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.version2.dk/artikel/surface-pro-2-fungerer-bedre-til-arbejde-end-fornoejelse-55195', + 'body' => array( + '//section[contains(@class, "teaser")]', + '//section[contains(@class, "body")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vezess.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vezess.hu.php new file mode 100644 index 0000000..acd68c0 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vezess.hu.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.vezess.hu/hirek/2017/10/20/audi-a7-2018-bemutato/', + 'body' => array( + '//article[@id="news"]/h1', + '//article[@id="news"]/h2', + '//article[@id="news"]/p[@class="lead"]', + '//article[@id="news"]/p[@class="main-pic responsive-img-container"]', + '//div[@class="article-body"]' + ), + 'strip' => array( + '//div[@class="info-bar"]', + '//ul[@class="breadcrumb"]', + '//div[@class="embed-link ce_widget"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vgcats.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vgcats.com.php new file mode 100644 index 0000000..b2830a3 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vgcats.com.php @@ -0,0 +1,15 @@ + array( + '%/comics.*%' => array( + 'test_url' => 'http://www.vgcats.com/comics/?strip_id=358', + 'body' => array('//*[@align="center"]/img'), + 'strip' => array(), + ), + '%/super.*%' => array( + 'test_url' => 'http://www.vgcats.com/super/?strip_id=84', + 'body' => array('//*[@align="center"]/p/img'), + 'strip' => array(), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vuxml.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vuxml.org.php new file mode 100644 index 0000000..b9bef7a --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/vuxml.org.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.vuxml.org/freebsd/a5f160fa-deee-11e4-99f8-080027ef73ec.html', + 'body' => array( + '//body', + ), + 'strip' => array( + '//h1', + '//div[@class="blurb"]', + '//hr', + '//p[@class="copyright"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/wausaudailyherald.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/wausaudailyherald.com.php new file mode 100644 index 0000000..58aceea --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/wausaudailyherald.com.php @@ -0,0 +1,27 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.wausaudailyherald.com/story/news/2017/04/01/hundreds-gather-remember-attorney-killed-shooting-spree/99826062/?from=global&sessionKey=&autologin=', + 'body' => array( + '//div[@itemprop="articleBody"]', + ), + 'strip' => array( + '//h1', + '//iframe', + '//span[@class="mycapture-small-btn mycapture-btn-with-text mycapture-expandable-photo-btn-small js-mycapture-btn-small"]', + '//div[@class="close-wrap"]', + '//div[contains(@class,"ui-video-wrapper")]', + '//div[contains(@class,"media-mob")]', + '//div[contains(@class,"left-mob")]', + '//div[contains(@class,"nerdbox")]', + '//p/span', + '//div[contains(@class,"oembed-asset")]', + '//*[contains(@class,"share")]', + '//div[contains(@class,"gallery-asset")]', + '//div[contains(@class,"oembed-asset")]', + '//div[@class="article-print-url"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/welt.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/welt.de.php new file mode 100644 index 0000000..83a2ebe --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/welt.de.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'https://www.welt.de/debatte/kommentare/article169740590/Bloss-keine-sozialdemokratische-Konsenssause.html', + 'body' => array( + '//main/article/header/div/div[contains(@class, "c-summary")]/div', + '//main/article/header/div[3]/div/figure/div/div/div/picture[1]', + '//main/article/header/div[3]/div/figure/figcaption/child::*', + '//main/article/div[contains(@class, "c-article-text")]' + ), + 'strip' => array( + '//*[contains(@class, "c-inline-element--has-commercials")]', + '//*[contains(@class, "c-inline-teaser")]', + '//figure[contains(@class, "c-video-element")]', + '//main/article/div[contains(@class, "c-article-text")]/div[@class="c-inline-element"]/div[contains(@class, "c-image-element")]' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/westfalen-blatt.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/westfalen-blatt.de.php new file mode 100644 index 0000000..adcf417 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/westfalen-blatt.de.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.westfalen-blatt.de/OWL/Lokales/Kreis-Hoexter/Warburg/3024113-Polizei-in-Warburg-Hier-waren-keine-kriminellen-Profis-am-Werk-Wurstautomat-Sprengung-mit-Polen-Boellern', + 'body' => array( + '//div[contains(@class, "articleimage")]', + '//div[@class="attribute-short"]', + '//div[@class="attribute-long"]', + ), + 'strip' => array( + '//div[@class="fb-post"]' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bbc.co.uk.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bbc.co.uk.php new file mode 100644 index 0000000..98fc368 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bbc.co.uk.php @@ -0,0 +1,33 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bbc.co.uk/news/world-middle-east-23911833', + 'body' => array( + '//div[@class="story-body__inner"] | //div[@class="article"]', + '//div[@class="indPost"]', + ), + 'strip' => array( + '//form', + '//div[@id="headline"]', + '//*[@class="warning"]', + '//span[@class="off-screen"]', + '//span[@class="story-image-copyright"]', + '//ul[@class="story-body__unordered-list"]', + '//div[@class="ad_wrapper"]', + '//div[@id="article-sidebar"]', + '//div[@class="data-table-outer"]', + '//*[@class="story-date"]', + '//*[@class="story-header"]', + '//figure[contains(@class,"has-caption")]', + '//*[@class="story-related"]', + '//*[contains(@class, "byline")]', + '//p[contains(@class, "media-message")]', + '//*[contains(@class, "story-feature")]', + '//*[@id="video-carousel-container"]', + '//*[@id="also-related-links"]', + '//*[contains(@class, "share") or contains(@class, "hidden") or contains(@class, "hyper")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bdgest.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bdgest.com.php new file mode 100644 index 0000000..41ef68d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bdgest.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bdgest.com/chronique-6027-BD-Adrastee-Tome-2.html', + 'body' => array( + '//*[contains(@class, "chronique")]', + ), + 'strip' => array( + '//*[contains(@class, "post-review")]', + '//*[contains(@class, "footer-review")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bgr.in.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bgr.in.php new file mode 100644 index 0000000..63ca069 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.bgr.in.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.bgr.in/news/xiaomi-redmi-3-with-13-megapixel-camera-snapdragon-616-launched-price-specifications-and-features/', + 'body' => array( + '//div[@class="article-content"]', + ), + 'strip' => array( + '//*[@class="article-meta"]', + '//*[@class="contentAdsense300"]', + '//*[@class="iwpl-social-hide"]', + '//iframe[@class="iframeads"]', + '//*[@class="disqus_thread"]', + '//*[@class="outb-mobile OUTBRAIN"]', + '//*[@class="wdt_smart_alerts"]', + '//*[@class="footnote"]', + '//*[@id="gadget-widget"]', + '//header[@class="article-title entry-header"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.businessweek.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.businessweek.com.php new file mode 100644 index 0000000..0acc44e --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.businessweek.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.businessweek.com/articles/2013-09-18/elon-musks-hyperloop-will-work-says-some-very-smart-software', + 'body' => array( + '//div[@id="lead_graphic"]', + '//div[@id="article_body"]', + ), + 'strip' => array( + '//*[contains(@class, "related_item")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.cnn.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.cnn.com.php new file mode 100644 index 0000000..31d03ed --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.cnn.com.php @@ -0,0 +1,24 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.cnn.com/2013/08/31/world/meast/syria-civil-war/index.html?hpt=hp_t1', + 'body' => array( + '//div[@class="cnn_strycntntlft"]', + ), + 'strip' => array( + '//div[@class="cnn_stryshrwdgtbtm"]', + '//div[@class="cnn_strybtmcntnt"]', + '//div[@class="cnn_strylftcntnt"]', + '//div[contains(@class, "cnnGalleryContainer")]', + '//div[contains(@class, "cnn_strylftcexpbx")]', + '//div[contains(@class, "articleGalleryNavContainer")]', + '//div[contains(@class, "cnnArticleGalleryCaptionControl")]', + '//div[contains(@class, "cnnArticleGalleryNavPrevNextDisabled")]', + '//div[contains(@class, "cnnArticleGalleryNavPrevNext")]', + '//div[contains(@class, "cnn_html_media_title_new")]', + '//div[contains(@id, "disqus")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.developpez.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.developpez.com.php new file mode 100644 index 0000000..1535e43 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.developpez.com.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.developpez.com/actu/81757/Mozilla-annonce-la-disponibilite-de-Firefox-36-qui-passe-au-HTTP-2-et-permet-la-synchronisation-de-son-ecran-d-accueil/', + 'body' => array( + '//*[@itemprop="articleBody"]', + ), + 'strip' => array( + '//form', + '//div[@class="content"]/img', + '//a[last()]/following-sibling::*', + '//*[contains(@class,"actuTitle")]', + '//*[contains(@class,"date")]', + '//*[contains(@class,"inlineimg")]', + '//*[@id="signaler"]', + '//*[@id="signalerFrame"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.egscomics.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.egscomics.com.php new file mode 100644 index 0000000..263f075 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.egscomics.com.php @@ -0,0 +1,12 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.egscomics.com/index.php?id=1690', + 'title' => '/html/head/title', + 'body' => array( + '//img[@id="comic"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.fakingnews.firstpost.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.fakingnews.firstpost.com.php new file mode 100644 index 0000000..c948c77 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.fakingnews.firstpost.com.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.fakingnews.firstpost.com/2016/01/engineering-student-creates-record-in-a-decade-becomes-the-first-to-completely-exhaust-ball-pen-refill/', + 'body' => array( + '//div[@class="entry"]', + ), + 'strip' => array( + '//*[@class="socialshare_bar"]', + '//*[@class="authorbox"]', + '//*[@class="cf5_rps"]', + '//*[@class="60563 fb-comments fb-social-plugin"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.forbes.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.forbes.com.php new file mode 100644 index 0000000..fd16ed5 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.forbes.com.php @@ -0,0 +1,20 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.forbes.com/sites/andygreenberg/2013/09/05/follow-the-bitcoins-how-we-got-busted-buying-drugs-on-silk-roads-black-market/', + 'body' => array( + '//div[@id="leftRail"]/div[contains(@class, body)]', + ), + 'strip' => array( + '//aside', + '//div[contains(@class, "entity_block")]', + '//div[contains(@class, "vestpocket") and not contains(@class, "body")]', + '//div[contains(@style, "display")]', + '//div[contains(@id, "comment")]', + '//div[contains(@class, "widget")]', + '//div[contains(@class, "pagination")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.franceculture.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.franceculture.fr.php new file mode 100644 index 0000000..f7ec0d8 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.franceculture.fr.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.franceculture.fr/emission-culture-eco-la-finance-aime-toujours-la-france-2016-01-08', + 'body' => array( + '//div[@class="text-zone"]', + ), + 'strip' => array( + '//ul[@class="tags"]', + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.futura-sciences.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.futura-sciences.com.php new file mode 100644 index 0000000..ea94a0f --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.futura-sciences.com.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.futura-sciences.com/magazines/espace/infos/actu/d/astronautique-curiosity-franchi-succes-dune-dingo-gap-52289/#xtor=RSS-8', + 'body' => array( + '//div[contains(@class, "content fiche-")]', + ), + 'strip' => array( + '//h1', + '//*[contains(@class, "content-date")]', + '//*[contains(@class, "diaporama")]', + '//*[contains(@class, "slider")]', + '//*[contains(@class, "cartouche")]', + '//*[contains(@class, "noprint")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.geekculture.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.geekculture.com.php new file mode 100644 index 0000000..3d0b6c7 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.geekculture.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.geekculture.com/joyoftech/joyarchives/2180.html', + 'body' => array( + '//p[contains(@class,"Maintext")][2]/a/img[contains(@src,"joyimages")]', + ), + 'strip' => array(), + ), + ), +); + diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.howtogeek.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.howtogeek.com.php new file mode 100644 index 0000000..6879e76 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.howtogeek.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.howtogeek.com/235283/what-is-a-wireless-hard-drive-and-should-i-get-one/', + 'body' => array( + '//div[@class="thecontent"]', + ), + 'strip' => array( + '//*[@class="relatedside"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lepoint.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lepoint.fr.php new file mode 100644 index 0000000..dcb7e48 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lepoint.fr.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.lepoint.fr/c-est-arrive-aujourd-hui/19-septembre-1783-pour-la-premiere-fois-un-mouton-un-canard-et-un-coq-s-envoient-en-l-air-devant-louis-xvi-18-09-2012-1507704_494.php', + 'body' => array( + '//article', + ), + 'strip' => array( + '//*[contains(@class, "info_article")]', + '//*[contains(@class, "fildariane_titre")]', + '//*[contains(@class, "entete2_article")]', + '//*[contains(@class, "signature_article")]', + '//*[contains(@id, "share")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lesnumeriques.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lesnumeriques.com.php new file mode 100644 index 0000000..0137e20 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.lesnumeriques.com.php @@ -0,0 +1,25 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.lesnumeriques.com/blender/kitchenaid-diamond-5ksb1585-p27473/test.html', + 'body' => array( + '//*[@id="product-content"]', + '//*[@id="news-content"]', + '//*[@id="article-content"]', + ), + 'strip' => array( + '//form', + '//div[contains(@class, "price-v4"])', + '//div[contains(@class, "authors-and-date")]', + '//div[contains(@class, "mini-product")]', + '//div[@id="articles-related-authors"]', + '//div[@id="tags-socials"]', + '//div[@id="user-reviews"]', + '//div[@id="product-reviews"]', + '//div[@id="publication-breadcrumbs-and-date"]', + '//div[@id="publication-breadcrumbs-and-date"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.mac4ever.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.mac4ever.com.php new file mode 100644 index 0000000..60bc1bd --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.mac4ever.com.php @@ -0,0 +1,13 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.mac4ever.com/actu/87392_video-quand-steve-jobs-et-bill-gates-jouaient-au-bachelor-avec-le-mac', + 'body' => array( + '//div[contains(@class, "news-news-content")]', + ), + 'strip' => array( + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.makeuseof.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.makeuseof.com.php new file mode 100644 index 0000000..a274564 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.makeuseof.com.php @@ -0,0 +1,18 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.makeuseof.com/tag/having-problems-with-audio-in-windows-10-heres-a-likely-fix/', + 'body' => array( + '//div[@class="entry"]', + ), + 'strip' => array( + '//*[@class="new_sharebar"]', + '//*[@class="author"]', + '//*[@class="wdt_grouvi"]', + '//*[@class="wdt_smart_alerts"]', + '//*[@class="modal fade grouvi"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.monsieur-le-chien.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.monsieur-le-chien.fr.php new file mode 100644 index 0000000..5f5e987 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.monsieur-le-chien.fr.php @@ -0,0 +1,11 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.monsieur-le-chien.fr/index.php?planche=672', + 'body' => array( + '//img[starts-with(@src, "i/planches/")]', + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.npr.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.npr.org.php new file mode 100644 index 0000000..ecc0213 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.npr.org.php @@ -0,0 +1,28 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.npr.org/blogs/thesalt/2013/09/17/223345977/auto-brewery-syndrome-apparently-you-can-make-beer-in-your-gut', + 'body' => array( + '//article[contains(@class,"story")]', + ), + 'strip' => array( + '//div[@class="story-tools"]', + '//h3[@class="slug"]', + '//div[@class="storytitle"]', + '//div[@id="story-meta"]', + '//a[@id="mainContent"]', + '//div[@class="credit-caption"]', + '//div[@class="enlarge_html"]', + '//button', + '//div[contains(@id,"pullquote")]', + '//div[contains(@class,"internallink")]', + '//div[contains(@class,"video")]', + '//div[@class="simplenodate"]', + '//div[contains(@class,"share-")]', + '//div[@class="tags"]', + '//aside' + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.numerama.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.numerama.com.php new file mode 100644 index 0000000..fe4971c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.numerama.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.numerama.com/sciences/125959-recherches-ladn-recompensees-nobel-de-chimie.html', + 'body' => array( + '//article', + ), + 'strip' => array( + '//footer', + '//section[@class="related-article"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.oneindia.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.oneindia.com.php new file mode 100644 index 0000000..320c214 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.oneindia.com.php @@ -0,0 +1,14 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.oneindia.com/india/b-luru-govt-likely-remove-word-eunuch-from-sec-36-a-karnataka-police-act-1981173.html', + 'body' => array( + '//div[@class="ecom-ad-content"]', + ), + 'strip' => array( + '//*[@id="view_cmtns"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.pseudo-sciences.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.pseudo-sciences.org.php new file mode 100644 index 0000000..9e467ed --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.pseudo-sciences.org.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.pseudo-sciences.org/spip.php?article2275', + 'body' => array( + '//div[@id="art_main"]', + ), + 'strip' => array( + '//div[@id="art_print"]', + '//div[@id="art_chapo"]', + '//img[@class="puce"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.sciencemag.org.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.sciencemag.org.php new file mode 100644 index 0000000..ae7a93a --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.sciencemag.org.php @@ -0,0 +1,16 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.sciencemag.org/news/2016/01/could-bright-foamy-wak$', + 'body' => array( + '//div[@class="row--hero"]', + '//article[contains(@class,"primary")]', + ), + 'strip' => array( + '//header[@class="article__header"]', + '//footer[@class="article__foot"]', + ), + ), + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.slate.fr.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.slate.fr.php new file mode 100644 index 0000000..8c8dc89 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.slate.fr.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.slate.fr/monde/77034/allemagne-2013-couacs-campagne', + 'body' => array( + '//div[@class="article_content"]', + ), + 'strip' => array( + '//*[@id="slate_associated_bn"]', + '//*[@id="ligatus-article"]', + '//*[@id="article_sidebar"]', + '//div[contains(@id, "reseaux")]', + '//*[contains(@class, "smart") or contains(@class, "article_tags") or contains(@class, "article_reactions")]', + '//*[contains(@class, "OUTBRAIN") or contains(@class, "related_item") or contains(@class, "share")]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.universfreebox.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.universfreebox.com.php new file mode 100644 index 0000000..0747d0f --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.universfreebox.com.php @@ -0,0 +1,15 @@ + array( + '%.*%' => array( + 'test_url' => 'http://www.universfreebox.com/article/24305/4G-Bouygues-Telecom-lance-une-vente-flash-sur-son-forfait-Sensation-3Go', + 'body' => array( + '//div[@id="corps_corps"]', + ), + 'strip' => array( + '//*[@id="formulaire"]', + '//*[@id="commentaire"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.zeit.de.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.zeit.de.php new file mode 100644 index 0000000..316c265 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/www.zeit.de.php @@ -0,0 +1,41 @@ + array( + '%^/zeit-magazin.*%' => array( + 'test_url' => 'http://www.zeit.de/zeit-magazin/2015/15/pegida-kathrin-oertel-lutz-bachmann', + 'body' => array( + '//article[@class="article"]', + ), + 'strip' => array( + '//header/div/h1', + '//header/div/div[@class="article__head__subtitle"]', + '//header/div/div[@class="article__column__author"]', + '//header/div/div[@class="article__column__author"]', + '//header/div/span[@class="article__head__meta-wrap"]', + '//form', + '//style', + '//div[contains(@class, "ad-tile")]', + '//div[@class="iqd-mobile-adplace"]', + '//div[@id="iq-artikelanker"]', + '//div[@id="js-social-services"]', + '//section[@id="js-comments"]', + '//aside', + ), + ), + '%.*%' => array( + 'test_url' => 'http://www.zeit.de/politik/ausland/2015-04/thessaloniki-krise-griechenland-yannis-boutaris/', + 'body' => array( + '//div[@class="article-body"]', + ), + 'strip' => array( + '//*[@class="articleheader"]', + '//*[@class="excerpt"]', + '//div[contains(@class, "ad")]', + '//div[@itemprop="video"]', + '//*[@class="articlemeta"]', + '//*[@class="articlemeta-clear"]', + '//*[@class="zol_inarticletools"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/xkcd.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/xkcd.com.php new file mode 100644 index 0000000..8495726 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/xkcd.com.php @@ -0,0 +1,8 @@ + array( + '%.*%' => array( + '%alt="(.+)" */>%' => '/>
    $1', + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ymatuhin.ru.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ymatuhin.ru.php new file mode 100644 index 0000000..9fd83f1 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/ymatuhin.ru.php @@ -0,0 +1,21 @@ + array( + '%.*%' => array( + 'test_url' => 'https://ymatuhin.ru/tools/git-default-editor/', + 'body' => array( + '//section', + ), + 'strip' => array( + "//script", + "//style", + "//h1", + "//time", + "//aside", + "/html/body/section/ul", + "//amp-iframe", + "/html/body/section/h4" + ), + ) + ) +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zarojel.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zarojel.hu.php new file mode 100644 index 0000000..36d3bdf --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zarojel.hu.php @@ -0,0 +1,19 @@ + array( + '%.*%' => array( + 'test_url' => 'https://zarojel.hu/meg-egyszer-a-foldi-ugyrol-is/', + 'body' => array( + '//div[@class="entry-category"]/h1', + '//div[@class="entry-content"]/div[@class="vc_row wpb_row vc_row-fluid"]' + ), + 'strip' => array( + '//ins[@class="adsbygoogle"]', + '//script', + '//figcaption', + '//p[contains(text(),"Kapcsolódó")]', + '//div[@class="wpb_wrapper"]/p[@class="entry-title"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zdnet.com.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zdnet.com.php new file mode 100644 index 0000000..79b35dd --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zdnet.com.php @@ -0,0 +1,23 @@ + array( + '%.*%' => array( + 'test_url' => 'http://zdnet.com.feedsportal.com/c/35462/f/675637/s/4a33c93e/sc/11/l/0L0Szdnet0N0Carticle0Cchina0Eus0Eagree0Eon0Ecybercrime0Ecooperation0Eamid0Econtinued0Etension0C0Tftag0FRSSbaffb68/story01.htm', + 'body' => array( + '//p[@class="summary"]', + '//div[contains(@class,"storyBody")]', + ), + 'strip' => array( + '//*[contains(@class,"ad-")]', + '//p/span', + '//script', + '//p[@class="summary"]', + '//div[contains(@class,"relatedContent")]', + '//div[contains(@class,"loader")]', + '//p[@class="photoDetails"]', + '//div[@class="thumbnailSlider"]', + '//div[@class="shortcodeGalleryWrapper"]', + ), + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zoom.hu.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zoom.hu.php new file mode 100644 index 0000000..3e38781 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Rules/zoom.hu.php @@ -0,0 +1,17 @@ + array( + '%.*%' => array( + 'test_url' => 'https://zoom.hu/2017/10/20/mar-nem-nyomoznak-a-vegrehajtok-botranyai-miatt', + 'body' => array( + '//div[@class="title-wrapper"]/h1', + '//div[@class="entry-excerpt"]', + '//div[@class="thumbnail-wrapper"]', + '//div[@id="entry-content-id"]' + ), + 'strip' => array( + '//div[@class="place first normal"]' + ) + ), + ), +); diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/CandidateParser.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/CandidateParser.php new file mode 100644 index 0000000..0f74b3d --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/CandidateParser.php @@ -0,0 +1,281 @@ +dom = XmlParser::getHtmlDocument(''.$html); + $this->xpath = new DOMXPath($this->dom); + } + + /** + * Get the relevant content with the list of potential attributes. + * + * @return string + */ + public function execute() + { + $content = $this->findContentWithCandidates(); + + if (strlen($content) < 200) { + $content = $this->findContentWithArticle(); + } + + if (strlen($content) < 50) { + $content = $this->findContentWithBody(); + } + + return $this->stripGarbage($content); + } + + /** + * Find content based on the list of tag candidates. + * + * @return string + */ + public function findContentWithCandidates() + { + foreach ($this->candidatesAttributes as $candidate) { + Logger::setMessage(get_called_class().': Try this candidate: "'.$candidate.'"'); + + $nodes = $this->xpath->query('//*[(contains(@class, "'.$candidate.'") or @id="'.$candidate.'") and not (contains(@class, "nav") or contains(@class, "page"))]'); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().': Find candidate "'.$candidate.'"'); + + return $this->dom->saveXML($nodes->item(0)); + } + } + + return ''; + } + + /** + * Find
    tag. + * + * @return string + */ + public function findContentWithArticle() + { + $nodes = $this->xpath->query('//article'); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().': Find
    tag'); + + return $this->dom->saveXML($nodes->item(0)); + } + + return ''; + } + + /** + * Find tag. + * + * @return string + */ + public function findContentWithBody() + { + $nodes = $this->xpath->query('//body'); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().' Find '); + + return $this->dom->saveXML($nodes->item(0)); + } + + return ''; + } + + /** + * Strip useless tags. + * + * @param string $content + * @return string + */ + public function stripGarbage($content) + { + $dom = XmlParser::getDomDocument($content); + + if ($dom !== false) { + $xpath = new DOMXPath($dom); + + $this->stripTags($xpath); + $this->stripAttributes($dom, $xpath); + + $content = $dom->saveXML($dom->documentElement); + } + + return $content; + } + + /** + * Remove blacklisted tags. + * + * @param DOMXPath $xpath + */ + public function stripTags(DOMXPath $xpath) + { + foreach ($this->stripTags as $tag) { + $nodes = $xpath->query('//'.$tag); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().': Strip tag: "'.$tag.'"'); + + foreach ($nodes as $node) { + $node->parentNode->removeChild($node); + } + } + } + } + + /** + * Remove blacklisted attributes. + * + * @param DomDocument $dom + * @param DOMXPath $xpath + */ + public function stripAttributes(DomDocument $dom, DOMXPath $xpath) + { + foreach ($this->stripAttributes as $attribute) { + $nodes = $xpath->query('//*[contains(@class, "'.$attribute.'") or contains(@id, "'.$attribute.'")]'); + + if ($nodes !== false && $nodes->length > 0) { + Logger::setMessage(get_called_class().': Strip attribute: "'.$attribute.'"'); + + foreach ($nodes as $node) { + if ($this->shouldRemove($dom, $node)) { + $node->parentNode->removeChild($node); + } + } + } + } + } + + /** + * Find link for next page of the article. + * + * @return string + */ + public function findNextLink() + { + return null; + } + + /** + * Return false if the node should not be removed. + * + * @param DomDocument $dom + * @param \DomNode $node + * @return bool + */ + public function shouldRemove(DomDocument $dom, $node) + { + $document_length = strlen($dom->textContent); + $node_length = strlen($node->textContent); + + if ($document_length === 0) { + return true; + } + + $ratio = $node_length * 100 / $document_length; + + if ($ratio >= 90) { + Logger::setMessage(get_called_class().': Should not remove this node ('.$node->nodeName.') ratio: '.$ratio.'%'); + + return false; + } + + return true; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/ParserInterface.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/ParserInterface.php new file mode 100644 index 0000000..3ded4b1 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/ParserInterface.php @@ -0,0 +1,20 @@ +getRulesFileList($hostname); + + foreach ($this->getRulesFolders() as $folder) { + $rule = $this->loadRuleFile($folder, $files); + + if (!empty($rule)) { + return $rule; + } + } + } + + return array(); + } + + /** + * Get the list of possible rules file names for a given hostname. + * + * @param string $hostname Hostname + * @return array + */ + public function getRulesFileList($hostname) + { + $files = array($hostname); // subdomain.domain.tld + $parts = explode('.', $hostname); + $len = count($parts); + + if ($len > 2) { + $subdomain = array_shift($parts); + $files[] = implode('.', $parts); // domain.tld + $files[] = '.'.implode('.', $parts); // .domain.tld + $files[] = $subdomain; // subdomain + } elseif ($len === 2) { + $files[] = '.'.implode('.', $parts); // .domain.tld + $files[] = $parts[0]; // domain + } + + return $files; + } + + /** + * Load a rule file from the defined folder. + * + * @param string $folder Rule directory + * @param array $files List of possible file names + * @return array + */ + public function loadRuleFile($folder, array $files) + { + foreach ($files as $file) { + $filename = $folder.'/'.$file.'.php'; + if (file_exists($filename)) { + Logger::setMessage(get_called_class().' Load rule: '.$file); + + return include $filename; + } + } + + return array(); + } + + /** + * Get the list of folders that contains rules. + * + * @return array + */ + public function getRulesFolders() + { + $folders = array(); + + if ($this->config !== null && $this->config->getGrabberRulesFolder() !== null) { + $folders[] = $this->config->getGrabberRulesFolder(); + } + + $folders[] = __DIR__ . '/../Rules'; + + return $folders; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/RuleParser.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/RuleParser.php new file mode 100644 index 0000000..9beb59c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/RuleParser.php @@ -0,0 +1,102 @@ +rules = $rules; + $this->dom = XmlParser::getHtmlDocument(''.$html); + $this->xpath = new DOMXPath($this->dom); + } + + /** + * Get the relevant content with predefined rules. + * + * @return string + */ + public function execute() + { + $this->stripTags(); + + return $this->findContent(); + } + + /** + * Remove HTML tags. + */ + public function stripTags() + { + if (isset($this->rules['strip']) && is_array($this->rules['strip'])) { + foreach ($this->rules['strip'] as $pattern) { + $nodes = $this->xpath->query($pattern); + + if ($nodes !== false && $nodes->length > 0) { + foreach ($nodes as $node) { + $node->parentNode->removeChild($node); + } + } + } + } + } + + /** + * Fetch content based on Xpath rules. + */ + public function findContent() + { + $content = ''; + if (isset($this->rules['body']) && is_array($this->rules['body'])) { + foreach ($this->rules['body'] as $pattern) { + $nodes = $this->xpath->query($pattern); + + if ($nodes !== false && $nodes->length > 0) { + foreach ($nodes as $node) { + $content .= $this->dom->saveXML($node); + } + } + } + } + + return $content; + } + + /** + * Fetch next link based on Xpath rules. + * + * @return string + */ + public function findNextLink() + { + if (isset($this->rules['next_page']) && is_array($this->rules['next_page'])) { + foreach ($this->rules['next_page'] as $pattern) { + $nodes = $this->xpath->query($pattern); + if ($nodes !== false && $nodes->length > 0) { + foreach ($nodes as $node) { + return $node->getAttribute('href'); + } + } + } + } + return null; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/Scraper.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/Scraper.php new file mode 100644 index 0000000..29383b2 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Scraper/Scraper.php @@ -0,0 +1,282 @@ +enableCandidateParser = false; + return $this; + } + + /** + * Get encoding. + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set encoding. + * + * @param string $encoding + * + * @return Scraper + */ + public function setEncoding($encoding) + { + $this->encoding = $encoding; + + return $this; + } + + /** + * Get URL to download. + * + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set URL to download. + * + * @param string $url URL + * + * @return Scraper + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * Return true if the scraper found relevant content. + * + * @return bool + */ + public function hasRelevantContent() + { + return !empty($this->content); + } + + /** + * Get relevant content. + * + * @return string + */ + public function getRelevantContent() + { + return $this->content; + } + + /** + * Get raw content (unfiltered). + * + * @return string + */ + public function getRawContent() + { + return $this->html; + } + + /** + * Set raw content (unfiltered). + * + * @param string $html + * + * @return Scraper + */ + public function setRawContent($html) + { + $this->html = $html; + + return $this; + } + + /** + * Get filtered relevant content. + * + * @return string + */ + public function getFilteredContent() + { + $filter = Filter::html($this->content, $this->url); + $filter->setConfig($this->config); + + return $filter->execute(); + } + + /** + * Download the HTML content. + * + * @return bool + */ + public function download() + { + if (!empty($this->url)) { + + // Clear everything + $this->html = ''; + $this->content = ''; + $this->encoding = ''; + + try { + $client = Client::getInstance(); + $client->setConfig($this->config); + $client->setTimeout($this->config->getGrabberTimeout()); + $client->setUserAgent($this->config->getGrabberUserAgent()); + $client->execute($this->url); + + $this->url = $client->getUrl(); + $this->html = $client->getContent(); + $this->encoding = $client->getEncoding(); + + return true; + } catch (ClientException $e) { + Logger::setMessage(get_called_class().': '.$e->getMessage()); + } + } + + return false; + } + + /** + * Execute the scraper. + * + * @param string $pageContent + * @param int $recursionDepth + */ + public function execute($pageContent = '', $recursionDepth = 0) + { + $this->html = ''; + $this->encoding = ''; + $this->content = ''; + $this->download(); + $this->prepareHtml(); + + $parser = $this->getParser(); + + if ($parser !== null) { + $maxRecursions = $this->config->getMaxRecursions(); + if(!isset($maxRecursions)){ + $maxRecursions = 25; + } + $pageContent .= $parser->execute(); + // check if there is a link to next page and recursively get content (max 25 pages) + if((($nextLink = $parser->findNextLink()) !== null) && $recursionDepth < $maxRecursions){ + $nextLink = Url::resolve($nextLink,$this->url); + $this->setUrl($nextLink); + $this->execute($pageContent,$recursionDepth+1); + } + else{ + $this->content = $pageContent; + } + Logger::setMessage(get_called_class().': Content length: '.strlen($this->content).' bytes'); + } + } + + /** + * Get the parser. + * + * @return ParserInterface + */ + public function getParser() + { + $ruleLoader = new RuleLoader($this->config); + $rules = $ruleLoader->getRules($this->url); + + if (!empty($rules['grabber'])) { + Logger::setMessage(get_called_class().': Parse content with rules'); + + foreach ($rules['grabber'] as $pattern => $rule) { + $url = new Url($this->url); + $sub_url = $url->getFullPath(); + + if (preg_match($pattern, $sub_url)) { + Logger::setMessage(get_called_class().': Matched url '.$sub_url); + return new RuleParser($this->html, $rule); + } + } + } elseif ($this->enableCandidateParser) { + Logger::setMessage(get_called_class().': Parse content with candidates'); + } + + return new CandidateParser($this->html); + } + + /** + * Normalize encoding and strip head tag. + */ + public function prepareHtml() + { + $html_encoding = XmlParser::getEncodingFromMetaTag($this->html); + + $this->html = Encoding::convert($this->html, $html_encoding ?: $this->encoding); + $this->html = Filter::stripHeadTags($this->html); + + Logger::setMessage(get_called_class().': HTTP Encoding "'.$this->encoding.'" ; HTML Encoding "'.$html_encoding.'"'); + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/Subscription.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/Subscription.php new file mode 100644 index 0000000..12eccfd --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/Subscription.php @@ -0,0 +1,175 @@ +title = $title; + return $this; + } + + /** + * Get title + * + * @access public + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Set feed URL + * + * @access public + * @param string $feedUrl + * @return Subscription + */ + public function setFeedUrl($feedUrl) + { + $this->feedUrl = $feedUrl; + return $this; + } + + /** + * Get feed URL + * + * @access public + * @return string + */ + public function getFeedUrl() + { + return $this->feedUrl; + } + + /** + * Set site URL + * + * @access public + * @param string $siteUrl + * @return Subscription + */ + public function setSiteUrl($siteUrl) + { + $this->siteUrl = $siteUrl; + return $this; + } + + /** + * Get site URL + * + * @access public + * @return string + */ + public function getSiteUrl() + { + return $this->siteUrl; + } + + /** + * Set category + * + * @access public + * @param string $category + * @return Subscription + */ + public function setCategory($category) + { + $this->category = $category; + return $this; + } + + /** + * Get category + * + * @access public + * @return string + */ + public function getCategory() + { + return $this->category; + } + + /** + * Set description + * + * @access public + * @param string $description + * @return Subscription + */ + public function setDescription($description) + { + $this->description = $description; + return $this; + } + + /** + * Get description + * + * @access public + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Set type + * + * @access public + * @param string $type + * @return Subscription + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + + /** + * Get type + * + * @access public + * @return string + */ + public function getType() + { + return $this->type; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionList.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionList.php new file mode 100644 index 0000000..b173f89 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionList.php @@ -0,0 +1,75 @@ +title = $title; + return $this; + } + + /** + * Get title + * + * @access public + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Add subscription + * + * @access public + * @param Subscription $subscription + * @return SubscriptionList + */ + public function addSubscription(Subscription $subscription) + { + $this->subscriptions[] = $subscription; + return $this; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListBuilder.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListBuilder.php new file mode 100644 index 0000000..838e4cb --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListBuilder.php @@ -0,0 +1,204 @@ +subscriptionList = $subscriptionList; + } + + /** + * Get object instance + * + * @static + * @access public + * @param SubscriptionList $subscriptionList + * @return SubscriptionListBuilder + */ + public static function create(SubscriptionList $subscriptionList) + { + return new static($subscriptionList); + } + + /** + * Build OPML feed + * + * @access public + * @param string $filename + * @return string + */ + public function build($filename = '') + { + $this->document = new DomDocument('1.0', 'UTF-8'); + $this->document->formatOutput = true; + + $opmlElement = $this->document->createElement('opml'); + $opmlElement->setAttribute('version', '1.0'); + + $headElement = $this->document->createElement('head'); + + if ($this->subscriptionList->getTitle() !== '') { + $titleElement = $this->document->createElement('title'); + $titleElement->appendChild($this->document->createTextNode($this->subscriptionList->getTitle())); + $headElement->appendChild($titleElement); + } + + $opmlElement->appendChild($headElement); + $opmlElement->appendChild($this->buildBody()); + $this->document->appendChild($opmlElement); + + if ($filename !== '') { + $this->document->save($filename); + return ''; + } + + return $this->document->saveXML(); + } + + /** + * Return true if the list has categories + * + * @access public + * @return bool + */ + public function hasCategories() + { + foreach ($this->subscriptionList->subscriptions as $subscription) { + if ($subscription->getCategory() !== '') { + return true; + } + } + + return false; + } + + /** + * Build OPML body + * + * @access protected + * @return DOMElement + */ + protected function buildBody() + { + $bodyElement = $this->document->createElement('body'); + + if ($this->hasCategories()) { + $this->buildCategories($bodyElement); + return $bodyElement; + } + + foreach ($this->subscriptionList->subscriptions as $subscription) { + $bodyElement->appendChild($this->buildSubscription($subscription)); + } + + return $bodyElement; + } + + /** + * Build categories section + * + * @access protected + * @param DOMElement $bodyElement + */ + protected function buildCategories(DOMElement $bodyElement) + { + $categories = $this->groupByCategories(); + + foreach ($categories as $category => $subscriptions) { + $bodyElement->appendChild($this->buildCategory($category, $subscriptions)); + } + } + + /** + * Build category tag + * + * @access protected + * @param string $category + * @param array $subscriptions + * @return DOMElement + */ + protected function buildCategory($category, array $subscriptions) + { + $outlineElement = $this->document->createElement('outline'); + $outlineElement->setAttribute('text', $category); + + foreach ($subscriptions as $subscription) { + $outlineElement->appendChild($this->buildSubscription($subscription)); + } + + return $outlineElement; + } + + /** + * Build subscription entry + * + * @access public + * @param Subscription $subscription + * @return DOMElement + */ + protected function buildSubscription(Subscription $subscription) + { + $outlineElement = $this->document->createElement('outline'); + $outlineElement->setAttribute('type', $subscription->getType() ?: 'rss'); + $outlineElement->setAttribute('text', $subscription->getTitle() ?: $subscription->getFeedUrl()); + $outlineElement->setAttribute('xmlUrl', $subscription->getFeedUrl()); + + if ($subscription->getTitle() !== '') { + $outlineElement->setAttribute('title', $subscription->getTitle()); + } + + if ($subscription->getDescription() !== '') { + $outlineElement->setAttribute('description', $subscription->getDescription()); + } + + if ($subscription->getSiteUrl() !== '') { + $outlineElement->setAttribute('htmlUrl', $subscription->getSiteUrl()); + } + + return $outlineElement; + } + + /** + * Group subscriptions by category + * + * @access private + * @return array + */ + private function groupByCategories() + { + $categories = array(); + + foreach ($this->subscriptionList->subscriptions as $subscription) { + $categories[$subscription->getCategory()][] = $subscription; + } + + return $categories; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListParser.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListParser.php new file mode 100644 index 0000000..9085588 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionListParser.php @@ -0,0 +1,100 @@ +subscriptionList = new SubscriptionList(); + $this->data = trim($data); + } + + /** + * Get object instance + * + * @static + * @access public + * @param string $data + * @return SubscriptionListParser + */ + public static function create($data) + { + return new static($data); + } + + /** + * Parse a subscription list entry + * + * @access public + * @throws MalformedXmlException + * @return SubscriptionList + */ + public function parse() + { + $xml = XmlParser::getSimpleXml($this->data); + + if (! $xml || !isset($xml->head) || !isset($xml->body)) { + throw new MalformedXmlException('Unable to parse OPML file: invalid XML'); + } + + $this->parseTitle($xml->head); + $this->parseEntries($xml->body); + + return $this->subscriptionList; + } + + /** + * Parse title + * + * @access protected + * @param SimpleXMLElement $xml + */ + protected function parseTitle(SimpleXMLElement $xml) + { + $this->subscriptionList->setTitle((string) $xml->title); + } + + /** + * Parse entries + * + * @access protected + * @param SimpleXMLElement $body + */ + private function parseEntries(SimpleXMLElement $body) + { + foreach ($body->outline as $outlineElement) { + if (isset($outlineElement->outline)) { + $this->parseEntries($outlineElement); + } else { + $this->subscriptionList->subscriptions[] = SubscriptionParser::create($body, $outlineElement)->parse(); + } + } + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionParser.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionParser.php new file mode 100644 index 0000000..caff07c --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Serialization/SubscriptionParser.php @@ -0,0 +1,142 @@ +parentElement = $parentElement; + $this->outlineElement = $outlineElement; + $this->subscription = new Subscription(); + } + + /** + * Get object instance + * + * @static + * @access public + * @param SimpleXMLElement $parentElement + * @param SimpleXMLElement $outlineElement + * @return SubscriptionParser + */ + public static function create(SimpleXMLElement $parentElement, SimpleXMLElement $outlineElement) + { + return new static($parentElement, $outlineElement); + } + + /** + * Parse subscription entry + * + * @access public + * @return Subscription + */ + public function parse() + { + $this->subscription->setCategory($this->findCategory()); + $this->subscription->setTitle($this->findTitle()); + $this->subscription->setFeedUrl($this->findFeedUrl()); + $this->subscription->setSiteUrl($this->findSiteUrl()); + $this->subscription->setType($this->findType()); + $this->subscription->setDescription($this->findDescription()); + + return $this->subscription; + } + + /** + * Find category. + * + * @access protected + * @return string + */ + protected function findCategory() + { + return isset($this->parentElement['text']) ? (string) $this->parentElement['text'] : ''; + } + + /** + * Find title. + * + * @access protected + * @return string + */ + protected function findTitle() + { + return isset($this->outlineElement['title']) ? (string) $this->outlineElement['title'] : (string) $this->outlineElement['text']; + } + + /** + * Find feed url. + * + * @access protected + * @return string + */ + protected function findFeedUrl() + { + return (string) $this->outlineElement['xmlUrl']; + } + + /** + * Find site url. + * + * @access protected + * @return string + */ + protected function findSiteUrl() + { + return isset($this->outlineElement['htmlUrl']) ? (string) $this->outlineElement['htmlUrl'] : $this->findFeedUrl(); + } + + /** + * Find type. + * + * @access protected + * @return string + */ + protected function findType() + { + return isset($this->outlineElement['version']) ? (string) $this->outlineElement['version'] : + isset($this->outlineElement['type']) ? (string) $this->outlineElement['type'] : 'rss'; + } + + /** + * Find description. + * + * @access protected + * @return string + */ + protected function findDescription() + { + return isset($this->outlineElement['description']) ? (string) $this->outlineElement['description'] : $this->findTitle(); + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomFeedBuilder.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomFeedBuilder.php new file mode 100644 index 0000000..34f3780 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomFeedBuilder.php @@ -0,0 +1,65 @@ +helper = new AtomHelper($this->getDocument()); + + $this->feedElement = $this->getDocument()->createElement('feed'); + $this->feedElement->setAttributeNodeNS(new DomAttr('xmlns', 'http://www.w3.org/2005/Atom')); + + $generator = $this->getDocument()->createElement('generator', 'PicoFeed'); + $generator->setAttribute('uri', 'https://github.com/miniflux/picoFeed'); + $this->feedElement->appendChild($generator); + + $this->helper + ->buildTitle($this->feedElement, $this->feedTitle) + ->buildId($this->feedElement, $this->feedUrl) + ->buildDate($this->feedElement, $this->feedDate) + ->buildLink($this->feedElement, $this->siteUrl) + ->buildLink($this->feedElement, $this->feedUrl, 'self', 'application/atom+xml') + ->buildAuthor($this->feedElement, $this->authorName, $this->authorEmail, $this->authorUrl) + ; + + foreach ($this->items as $item) { + $this->feedElement->appendChild($item->build()); + } + + $this->getDocument()->appendChild($this->feedElement); + + if ($filename !== '') { + $this->getDocument()->save($filename); + } + + return $this->getDocument()->saveXML(); + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomHelper.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomHelper.php new file mode 100644 index 0000000..def6b0b --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomHelper.php @@ -0,0 +1,139 @@ +document = $document; + } + + /** + * Build node + * + * @access public + * @param DOMElement $element + * @param string $tag + * @param string $value + * @return AtomHelper + */ + public function buildNode(DOMElement $element, $tag, $value) + { + $node = $this->document->createElement($tag); + $node->appendChild($this->document->createTextNode($value)); + $element->appendChild($node); + return $this; + } + + /** + * Build title + * + * @access public + * @param DOMElement $element + * @param string $title + * @return AtomHelper + */ + public function buildTitle(DOMElement $element, $title) + { + return $this->buildNode($element, 'title', $title); + } + + /** + * Build id + * + * @access public + * @param DOMElement $element + * @param string $id + * @return AtomHelper + */ + public function buildId(DOMElement $element, $id) + { + return $this->buildNode($element, 'id', $id); + } + + /** + * Build date element + * + * @access public + * @param DOMElement $element + * @param DateTime $date + * @param string $type + * @return AtomHelper + */ + public function buildDate(DOMElement $element, DateTime $date, $type = 'updated') + { + return $this->buildNode($element, $type, $date->format(DateTime::ATOM)); + } + + /** + * Build link element + * + * @access public + * @param DOMElement $element + * @param string $url + * @param string $rel + * @param string $type + * @return AtomHelper + */ + public function buildLink(DOMElement $element, $url, $rel = 'alternate', $type = 'text/html') + { + $node = $this->document->createElement('link'); + $node->setAttribute('rel', $rel); + $node->setAttribute('type', $type); + $node->setAttribute('href', $url); + $element->appendChild($node); + + return $this; + } + + /** + * Build author element + * + * @access public + * @param DOMElement $element + * @param string $authorName + * @param string $authorEmail + * @param string $authorUrl + * @return AtomHelper + */ + public function buildAuthor(DOMElement $element, $authorName, $authorEmail, $authorUrl) + { + if (!empty($authorName)) { + $author = $this->document->createElement('author'); + $this->buildNode($author, 'name', $authorName); + + if (!empty($authorEmail)) { + $this->buildNode($author, 'email', $authorEmail); + } + + if (!empty($authorUrl)) { + $this->buildNode($author, 'uri', $authorUrl); + } + + $element->appendChild($author); + } + + return $this; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomItemBuilder.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomItemBuilder.php new file mode 100644 index 0000000..dfdfe68 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/AtomItemBuilder.php @@ -0,0 +1,63 @@ +itemElement = $this->feedBuilder->getDocument()->createElement('entry'); + $this->helper = new AtomHelper($this->feedBuilder->getDocument()); + + if (!empty($this->itemId)) { + $this->helper->buildId($this->itemElement, $this->itemId); + } else { + $this->helper->buildId($this->itemElement, $this->itemUrl); + } + + $this->helper + ->buildTitle($this->itemElement, $this->itemTitle) + ->buildLink($this->itemElement, $this->itemUrl) + ->buildDate($this->itemElement, $this->itemUpdatedDate, 'updated') + ->buildDate($this->itemElement, $this->itemPublishedDate, 'published') + ->buildAuthor($this->itemElement, $this->authorName, $this->authorEmail, $this->authorUrl) + ; + + if (!empty($this->itemSummary)) { + $this->helper->buildNode($this->itemElement, 'summary', $this->itemSummary); + } + + if (!empty($this->itemContent)) { + $node = $this->feedBuilder->getDocument()->createElement('content'); + $node->setAttribute('type', 'html'); + $node->appendChild($this->feedBuilder->getDocument()->createCDATASection($this->itemContent)); + $this->itemElement->appendChild($node); + } + + return $this->itemElement; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/FeedBuilder.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/FeedBuilder.php new file mode 100644 index 0000000..cf9d024 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/FeedBuilder.php @@ -0,0 +1,185 @@ +document = new DomDocument('1.0', 'UTF-8'); + $this->document->formatOutput = true; + } + + /** + * Get new object instance + * + * @access public + * @return static + */ + public static function create() + { + return new static(); + } + + /** + * Add feed title + * + * @access public + * @param string $title + * @return $this + */ + public function withTitle($title) + { + $this->feedTitle = $title; + return $this; + } + + /** + * Add feed url + * + * @access public + * @param string $url + * @return $this + */ + public function withFeedUrl($url) + { + $this->feedUrl = $url; + return $this; + } + + /** + * Add website url + * + * @access public + * @param string $url + * @return $this + */ + public function withSiteUrl($url) + { + $this->siteUrl = $url; + return $this; + } + + /** + * Add feed date + * + * @access public + * @param DateTime $date + * @return $this + */ + public function withDate(DateTime $date) + { + $this->feedDate = $date; + return $this; + } + + /** + * Add feed author + * + * @access public + * @param string $name + * @param string $email + * @param string $url + * @return $this + */ + public function withAuthor($name, $email = '', $url ='') + { + $this->authorName = $name; + $this->authorEmail = $email; + $this->authorUrl = $url; + return $this; + } + + /** + * Add feed item + * + * @access public + * @param ItemBuilder $item + * @return $this + */ + public function withItem(ItemBuilder $item) + { + $this->items[] = $item; + return $this; + } + + /** + * Get DOM document + * + * @access public + * @return DOMDocument + */ + public function getDocument() + { + return $this->document; + } + + /** + * Build feed + * + * @abstract + * @access public + * @param string $filename + * @return string + */ + abstract public function build($filename = ''); +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/ItemBuilder.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/ItemBuilder.php new file mode 100644 index 0000000..86985bc --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/ItemBuilder.php @@ -0,0 +1,209 @@ +feedBuilder = $feedBuilder; + } + + /** + * Get new object instance + * + * @access public + * @param FeedBuilder $feedBuilder + * @return static + */ + public static function create(FeedBuilder $feedBuilder) + { + return new static($feedBuilder); + } + + /** + * Add item title + * + * @access public + * @param string $title + * @return $this + */ + public function withTitle($title) + { + $this->itemTitle = $title; + return $this; + } + + /** + * Add item id + * + * @access public + * @param string $id + * @return $this + */ + public function withId($id) + { + $this->itemId = $id; + return $this; + } + + /** + * Add item url + * + * @access public + * @param string $url + * @return $this + */ + public function withUrl($url) + { + $this->itemUrl = $url; + return $this; + } + + /** + * Add item summary + * + * @access public + * @param string $summary + * @return $this + */ + public function withSummary($summary) + { + $this->itemSummary = $summary; + return $this; + } + + /** + * Add item content + * + * @access public + * @param string $content + * @return $this + */ + public function withContent($content) + { + $this->itemContent = $content; + return $this; + } + + /** + * Add item updated date + * + * @access public + * @param DateTime $date + * @return $this + */ + public function withUpdatedDate(DateTime $date) + { + $this->itemUpdatedDate = $date; + return $this; + } + + /** + * Add item published date + * + * @access public + * @param DateTime $date + * @return $this + */ + public function withPublishedDate(DateTime $date) + { + $this->itemPublishedDate = $date; + return $this; + } + + /** + * Add item author + * + * @access public + * @param string $name + * @param string $email + * @param string $url + * @return $this + */ + public function withAuthor($name, $email = '', $url ='') + { + $this->authorName = $name; + $this->authorEmail = $email; + $this->authorUrl = $url; + return $this; + } + + /** + * Build item + * + * @abstract + * @access public + * @return DOMElement + */ + abstract public function build(); +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20FeedBuilder.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20FeedBuilder.php new file mode 100644 index 0000000..bc3f513 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20FeedBuilder.php @@ -0,0 +1,76 @@ +helper = new Rss20Helper($this->getDocument()); + + $this->rssElement = $this->getDocument()->createElement('rss'); + $this->rssElement->setAttribute('version', '2.0'); + $this->rssElement->setAttributeNodeNS(new DomAttr('xmlns:content', 'http://purl.org/rss/1.0/modules/content/')); + $this->rssElement->setAttributeNodeNS(new DomAttr('xmlns:atom', 'http://www.w3.org/2005/Atom')); + + $this->channelElement = $this->getDocument()->createElement('channel'); + $this->helper + ->buildNode($this->channelElement, 'generator', 'PicoFeed (https://github.com/miniflux/picoFeed)') + ->buildTitle($this->channelElement, $this->feedTitle) + ->buildNode($this->channelElement, 'description', $this->feedTitle) + ->buildDate($this->channelElement, $this->feedDate) + ->buildAuthor($this->channelElement, 'webMaster', $this->authorName, $this->authorEmail) + ->buildLink($this->channelElement, $this->siteUrl) + ; + + $link = $this->getDocument()->createElement('atom:link'); + $link->setAttribute('href', $this->feedUrl); + $link->setAttribute('rel', 'self'); + $link->setAttribute('type', 'application/rss+xml'); + $this->channelElement->appendChild($link); + + foreach ($this->items as $item) { + $this->channelElement->appendChild($item->build()); + } + + $this->rssElement->appendChild($this->channelElement); + $this->getDocument()->appendChild($this->rssElement); + + if ($filename !== '') { + $this->getDocument()->save($filename); + } + + return $this->getDocument()->saveXML(); + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20Helper.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20Helper.php new file mode 100644 index 0000000..72a19e5 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20Helper.php @@ -0,0 +1,115 @@ +document = $document; + } + + /** + * Build node + * + * @access public + * @param DOMElement $element + * @param string $tag + * @param string $value + * @return $this + */ + public function buildNode(DOMElement $element, $tag, $value) + { + $node = $this->document->createElement($tag); + $node->appendChild($this->document->createTextNode($value)); + $element->appendChild($node); + return $this; + } + + /** + * Build title + * + * @access public + * @param DOMElement $element + * @param string $title + * @return $this + */ + public function buildTitle(DOMElement $element, $title) + { + return $this->buildNode($element, 'title', $title); + } + + /** + * Build date element + * + * @access public + * @param DOMElement $element + * @param DateTime $date + * @param string $type + * @return $this + */ + public function buildDate(DOMElement $element, DateTime $date, $type = 'pubDate') + { + return $this->buildNode($element, $type, $date->format(DateTime::RSS)); + } + + /** + * Build link element + * + * @access public + * @param DOMElement $element + * @param string $url + * @return $this + */ + public function buildLink(DOMElement $element, $url) + { + return $this->buildNode($element, 'link', $url); + } + + /** + * Build author element + * + * @access public + * @param DOMElement $element + * @param string $tag + * @param string $authorName + * @param string $authorEmail + * @return $this + */ + public function buildAuthor(DOMElement $element, $tag, $authorName, $authorEmail) + { + if (!empty($authorName)) { + $value = ''; + + if (!empty($authorEmail)) { + $value .= $authorEmail.' ('.$authorName.')'; + } else { + $value = $authorName; + } + + $this->buildNode($element, $tag, $value); + } + + return $this; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20ItemBuilder.php b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20ItemBuilder.php new file mode 100644 index 0000000..125dc6a --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/lib/PicoFeed/Syndication/Rss20ItemBuilder.php @@ -0,0 +1,67 @@ +itemElement = $this->feedBuilder->getDocument()->createElement('item'); + $this->helper = new Rss20Helper($this->feedBuilder->getDocument()); + + if (!empty($this->itemId)) { + $guid = $this->feedBuilder->getDocument()->createElement('guid'); + $guid->setAttribute('isPermaLink', 'false'); + $guid->appendChild($this->feedBuilder->getDocument()->createTextNode($this->itemId)); + $this->itemElement->appendChild($guid); + } else { + $guid = $this->feedBuilder->getDocument()->createElement('guid'); + $guid->setAttribute('isPermaLink', 'true'); + $guid->appendChild($this->feedBuilder->getDocument()->createTextNode($this->itemUrl)); + $this->itemElement->appendChild($guid); + } + + $this->helper + ->buildTitle($this->itemElement, $this->itemTitle) + ->buildLink($this->itemElement, $this->itemUrl) + ->buildDate($this->itemElement, $this->itemPublishedDate) + ->buildAuthor($this->itemElement, 'author', $this->authorName, $this->authorEmail) + ; + + if (!empty($this->itemSummary)) { + $this->helper->buildNode($this->itemElement, 'description', $this->itemSummary); + } + + if (!empty($this->itemContent)) { + $node = $this->feedBuilder->getDocument()->createElement('content:encoded'); + $node->appendChild($this->feedBuilder->getDocument()->createCDATASection($this->itemContent)); + $this->itemElement->appendChild($node); + } + + return $this->itemElement; + } +} diff --git a/user/plugins/admin/vendor/p3k/picofeed/picofeed b/user/plugins/admin/vendor/p3k/picofeed/picofeed new file mode 100644 index 0000000..8f35737 --- /dev/null +++ b/user/plugins/admin/vendor/p3k/picofeed/picofeed @@ -0,0 +1,135 @@ +#!/usr/bin/env php +discover($url); + + $parser = $reader->getParser( + $resource->getUrl(), + $resource->getContent(), + $resource->getEncoding() + ); + + if ($disable_filtering) { + $parser->disableContentFiltering(); + } + + return $parser->execute(); + } + catch (PicoFeedException $e) { + echo 'Exception thrown ===> "'.$e->getMessage().'"'.PHP_EOL; + return false; + } +} + +function get_item($feed, $item_id) +{ + foreach ($feed->items as $item) { + if ($item->getId() === $item_id) { + echo $item; + echo "============= CONTENT ================\n"; + echo $item->getContent(); + echo "\n============= CONTENT ================\n"; + break; + } + } +} + +function dump_feed($url) +{ + $feed = get_feed($url); + echo $feed; +} + +function debug_feed($url) +{ + get_feed($url); + print_r(Logger::getMessages()); +} + +function dump_item($url, $item_id) +{ + $feed = get_feed($url); + + if ($feed !== false) { + get_item($feed, $item_id); + } +} + +function nofilter_item($url, $item_id) +{ + $feed = get_feed($url, true); + + if ($feed !== false) { + get_item($feed, $item_id); + } +} + +function grabber($url) +{ + $grabber = new Scraper(new Config); + $grabber->setUrl($url); + $grabber->execute(); + + print_r(Logger::getMessages()); + echo "============= CONTENT ================\n"; + echo $grabber->getRelevantContent().PHP_EOL; + echo "============= FILTERED ================\n"; + echo $grabber->getFilteredContent().PHP_EOL; +} + +function fetch_favicon($url) +{ + $favicon = new Favicon(); + echo $favicon->find($url) . PHP_EOL; +} + +// Parse command line arguments +if ($argc === 4) { + switch ($argv[1]) { + case 'item': + dump_item($argv[2], $argv[3]); + die; + case 'nofilter': + nofilter_item($argv[2], $argv[3]); + die; + } +} else if ($argc === 3) { + switch ($argv[1]) { + case 'feed': + dump_feed($argv[2]); + die; + case 'debug': + debug_feed($argv[2]); + die; + case 'grabber': + grabber($argv[2]); + die; + case 'favicon': + fetch_favicon($argv[2]); + die; + } +} + +printf("Usage:\n"); +printf("%s feed \n", $argv[0]); +printf("%s debug \n", $argv[0]); +printf("%s item \n", $argv[0]); +printf("%s nofilter \n", $argv[0]); +printf("%s grabber \n", $argv[0]); +printf("%s favicon \n", $argv[0]); diff --git a/user/plugins/admin/vendor/scssphp/scssphp/LICENSE.md b/user/plugins/admin/vendor/scssphp/scssphp/LICENSE.md new file mode 100644 index 0000000..afcfdfb --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2015 Leaf Corcoran, http://scssphp.github.io/scssphp + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/user/plugins/admin/vendor/scssphp/scssphp/README.md b/user/plugins/admin/vendor/scssphp/scssphp/README.md new file mode 100644 index 0000000..65bb93e --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/README.md @@ -0,0 +1,71 @@ +# scssphp +### + +![Build](https://github.com/scssphp/scssphp/workflows/CI/badge.svg) +[![License](https://poser.pugx.org/scssphp/scssphp/license)](https://packagist.org/packages/scssphp/scssphp) + +`scssphp` is a compiler for SCSS written in PHP. + +Checkout the homepage, , for directions on how to use. + +## Running Tests + +`scssphp` uses [PHPUnit](https://github.com/sebastianbergmann/phpunit) for testing. + +Run the following command from the root directory to run every test: + + vendor/bin/phpunit tests + +There are several tests in the `tests/` directory: + +* `ApiTest.php` contains various unit tests that test the PHP interface. +* `ExceptionTest.php` contains unit tests that test for exceptions thrown by the parser and compiler. +* `FailingTest.php` contains tests reported in Github issues that demonstrate compatibility bugs. +* `InputTest.php` compiles every `.scss` file in the `tests/inputs` directory + then compares to the respective `.css` file in the `tests/outputs` directory. +* `SassSpecTest.php` extracts tests from the `sass/sass-spec` repository. + +When changing any of the tests in `tests/inputs`, the tests will most likely +fail because the output has changed. Once you verify that the output is correct +you can run the following command to rebuild all the tests: + + BUILD=1 vendor/bin/phpunit tests + +This will compile all the tests, and save results into `tests/outputs`. It also +updates the list of excluded specs from sass-spec. + +To enable the full `sass-spec` compatibility tests: + + TEST_SASS_SPEC=1 vendor/bin/phpunit tests + +## Coding Standard + +`scssphp` source conforms to [PSR12](https://www.php-fig.org/psr/psr-12/). + +Run the following command from the root directory to check the code for "sniffs". + + vendor/bin/phpcs --standard=PSR12 --extensions=php bin src tests *.php + +## Static Analysis + +`scssphp` uses [phpstan](https://phpstan.org/) for static analysis. + +Run the following command from the root directory to analyse the codebase: + + make phpstan + +As most of the codebase is composed of legacy code which cannot be type-checked +fully, the setup contains a baseline file with all errors we want to ignore. In +particular, we ignore all errors related to not specifying the types inside arrays +when these arrays correspond to the representation of Sass values and Sass AST nodes +in the parser and compiler. +When contributing, the proper process to deal with static analysis is the following: + +1. Make your change in the codebase +2. Run `make phpstan` +3. Fix errors reported by phpstan when possible +4. Repeat step 2 and 3 until nothing gets fixed anymore at step 3 +5. Run `make phpstan-baseline` to regenerate the phpstan baseline + +Additions to the baseline will be reviewed to avoid ignoring errors that should have +been fixed. diff --git a/user/plugins/admin/vendor/scssphp/scssphp/bin/pscss b/user/plugins/admin/vendor/scssphp/scssphp/bin/pscss new file mode 100644 index 0000000..e622398 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/bin/pscss @@ -0,0 +1,244 @@ +#!/usr/bin/env php +parse($data)), true)); + + fwrite(STDERR, 'Warning: the --dump-tree option is deprecated. Use proper debugging tools instead.'); + + exit(); +} + +$scss = new Compiler(); + +if ($loadPaths) { + $scss->setImportPaths($loadPaths); +} + +if ($style) { + if ($style === OutputStyle::COMPRESSED || $style === OutputStyle::EXPANDED) { + $scss->setOutputStyle($style); + } else { + fwrite(STDERR, "WARNING: the $style style is deprecated.\n"); + $scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style)); + } +} + +$outputFile = isset($arguments[1]) ? $arguments[1] : null; +$sourceMapFile = null; + +if ($sourceMap) { + $sourceMapOptions = array( + 'outputSourceFiles' => $embedSources, + ); + if ($embedSourceMap || $outputFile === null) { + $scss->setSourceMap(Compiler::SOURCE_MAP_INLINE); + } else { + $sourceMapFile = $outputFile . '.map'; + $sourceMapOptions['sourceMapWriteTo'] = $sourceMapFile; + $sourceMapOptions['sourceMapURL'] = basename($sourceMapFile); + $sourceMapOptions['sourceMapBasepath'] = getcwd(); + $sourceMapOptions['sourceMapFilename'] = basename($outputFile); + + $scss->setSourceMap(Compiler::SOURCE_MAP_FILE); + } + + $scss->setSourceMapOptions($sourceMapOptions); +} + +if ($encoding) { + $scss->setEncoding($encoding); +} + +try { + $result = $scss->compileString($data, $inputFile); +} catch (SassException $e) { + fwrite(STDERR, 'Error: '.$e->getMessage()."\n"); + exit(1); +} + +if ($outputFile) { + file_put_contents($outputFile, $result->getCss()); + + if ($sourceMapFile !== null && $result->getSourceMap() !== null) { + file_put_contents($sourceMapFile, $result->getSourceMap()); + } +} else { + echo $result->getCss(); +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/composer.json b/user/plugins/admin/vendor/scssphp/scssphp/composer.json new file mode 100644 index 0000000..86cd396 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/composer.json @@ -0,0 +1,108 @@ +{ + "name": "scssphp/scssphp", + "type": "library", + "description": "scssphp is a compiler for SCSS written in PHP.", + "keywords": ["css", "stylesheet", "scss", "sass", "less"], + "homepage": "http://scssphp.github.io/scssphp/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthon Pang", + "email": "apang@softwaredevelopment.ca", + "homepage": "https://github.com/robocoder" + }, + { + "name": "Cédric Morin", + "email": "cedric@yterium.com", + "homepage": "https://github.com/Cerdic" + } + ], + "autoload": { + "psr-4": { "ScssPhp\\ScssPhp\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "ScssPhp\\ScssPhp\\Tests\\": "tests/" } + }, + "require": { + "php": ">=5.6.0", + "ext-json": "*", + "ext-ctype": "*" + }, + "suggest": { + "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv", + "ext-iconv": "Can be used as fallback when ext-mbstring is not available" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", + "sass/sass-spec": "*", + "squizlabs/php_codesniffer": "~3.5", + "symfony/phpunit-bridge": "^5.1", + "thoughtbot/bourbon": "^7.0", + "twbs/bootstrap": "~5.0", + "twbs/bootstrap4": "4.6.0", + "zurb/foundation": "~6.5" + }, + "repositories": [ + { + "type": "package", + "package": { + "name": "sass/sass-spec", + "version": "2021.09.15", + "source": { + "type": "git", + "url": "https://github.com/sass/sass-spec.git", + "reference": "eb2d7a0865c1faf0b55a39ff962b24aca9b4c955" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sass/sass-spec/zipball/eb2d7a0865c1faf0b55a39ff962b24aca9b4c955", + "reference": "eb2d7a0865c1faf0b55a39ff962b24aca9b4c955", + "shasum": "" + } + } + }, + { + "type": "package", + "package": { + "name": "thoughtbot/bourbon", + "version": "v7.0.0", + "source": { + "type": "git", + "url": "https://github.com/thoughtbot/bourbon.git", + "reference": "fbe338ee6807e7f7aa996d82c8a16f248bb149b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thoughtbot/bourbon/zipball/fbe338ee6807e7f7aa996d82c8a16f248bb149b3", + "reference": "fbe338ee6807e7f7aa996d82c8a16f248bb149b3", + "shasum": "" + } + } + }, + { + "type": "package", + "package": { + "name": "twbs/bootstrap4", + "version": "v4.6.0", + "source": { + "type": "git", + "url": "https://github.com/twbs/bootstrap.git", + "reference": "6ffb0b48e455430f8a5359ed689ad64c1143fac2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/6ffb0b48e455430f8a5359ed689ad64c1143fac2", + "reference": "6ffb0b48e455430f8a5359ed689ad64c1143fac2", + "shasum": "" + } + } + } + ], + "bin": ["bin/pscss"], + "config": { + "sort-packages": true + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/phpcs.xml.dist b/user/plugins/admin/vendor/scssphp/scssphp/phpcs.xml.dist new file mode 100644 index 0000000..b162dbd --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/phpcs.xml.dist @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/user/plugins/admin/vendor/scssphp/scssphp/scss.inc.php b/user/plugins/admin/vendor/scssphp/scssphp/scss.inc.php new file mode 100644 index 0000000..4598378 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/scss.inc.php @@ -0,0 +1,21 @@ + + * + * @internal + */ +class Range +{ + /** + * @var float|int + */ + public $first; + + /** + * @var float|int + */ + public $last; + + /** + * Initialize range + * + * @param integer|float $first + * @param integer|float $last + */ + public function __construct($first, $last) + { + $this->first = $first; + $this->last = $last; + } + + /** + * Test for inclusion in range + * + * @param integer|float $value + * + * @return boolean + */ + public function includes($value) + { + return $value >= $this->first && $value <= $this->last; + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Block.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Block.php new file mode 100644 index 0000000..3ae49d0 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Block.php @@ -0,0 +1,73 @@ + + * + * @internal + */ +class Block +{ + /** + * @var string + */ + public $type; + + /** + * @var \ScssPhp\ScssPhp\Block + */ + public $parent; + + /** + * @var string + */ + public $sourceName; + + /** + * @var integer + */ + public $sourceIndex; + + /** + * @var integer + */ + public $sourceLine; + + /** + * @var integer + */ + public $sourceColumn; + + /** + * @var array|null + */ + public $selectors; + + /** + * @var array + */ + public $comments; + + /** + * @var array + */ + public $children; + + /** + * @var \ScssPhp\ScssPhp\Block|null + */ + public $selfParent; +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Cache.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Cache.php new file mode 100644 index 0000000..9731c60 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Cache.php @@ -0,0 +1,272 @@ + + * + * @internal + */ +class Cache +{ + const CACHE_VERSION = 1; + + /** + * directory used for storing data + * + * @var string|false + */ + public static $cacheDir = false; + + /** + * prefix for the storing data + * + * @var string + */ + public static $prefix = 'scssphp_'; + + /** + * force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit + * + * @var bool|string + */ + public static $forceRefresh = false; + + /** + * specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up + * + * @var int + */ + public static $gcLifetime = 604800; + + /** + * array of already refreshed cache if $forceRefresh==='once' + * + * @var array + */ + protected static $refreshed = []; + + /** + * Constructor + * + * @param array $options + * + * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string} $options + */ + public function __construct($options) + { + // check $cacheDir + if (isset($options['cacheDir'])) { + self::$cacheDir = $options['cacheDir']; + } + + if (empty(self::$cacheDir)) { + throw new Exception('cacheDir not set'); + } + + if (isset($options['prefix'])) { + self::$prefix = $options['prefix']; + } + + if (empty(self::$prefix)) { + throw new Exception('prefix not set'); + } + + if (isset($options['forceRefresh'])) { + self::$forceRefresh = $options['forceRefresh']; + } + + self::checkCacheDir(); + } + + /** + * Get the cached result of $operation on $what, + * which is known as dependant from the content of $options + * + * @param string $operation parse, compile... + * @param mixed $what content key (e.g., filename to be treated) + * @param array $options any option that affect the operation result on the content + * @param int|null $lastModified last modified timestamp + * + * @return mixed + * + * @throws \Exception + */ + public function getCache($operation, $what, $options = [], $lastModified = null) + { + $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options); + + if ( + ((self::$forceRefresh === false) || (self::$forceRefresh === 'once' && + isset(self::$refreshed[$fileCache]))) && file_exists($fileCache) + ) { + $cacheTime = filemtime($fileCache); + + if ( + (\is_null($lastModified) || $cacheTime > $lastModified) && + $cacheTime + self::$gcLifetime > time() + ) { + $c = file_get_contents($fileCache); + $c = unserialize($c); + + if (\is_array($c) && isset($c['value'])) { + return $c['value']; + } + } + } + + return null; + } + + /** + * Put in cache the result of $operation on $what, + * which is known as dependant from the content of $options + * + * @param string $operation + * @param mixed $what + * @param mixed $value + * @param array $options + * + * @return void + */ + public function setCache($operation, $what, $value, $options = []) + { + $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options); + + $c = ['value' => $value]; + $c = serialize($c); + + file_put_contents($fileCache, $c); + + if (self::$forceRefresh === 'once') { + self::$refreshed[$fileCache] = true; + } + } + + /** + * Get the cache name for the caching of $operation on $what, + * which is known as dependant from the content of $options + * + * @param string $operation + * @param mixed $what + * @param array $options + * + * @return string + */ + private static function cacheName($operation, $what, $options = []) + { + $t = [ + 'version' => self::CACHE_VERSION, + 'scssphpVersion' => Version::VERSION, + 'operation' => $operation, + 'what' => $what, + 'options' => $options + ]; + + $t = self::$prefix + . sha1(json_encode($t)) + . ".$operation" + . ".scsscache"; + + return $t; + } + + /** + * Check that the cache dir exists and is writeable + * + * @return void + * + * @throws \Exception + */ + public static function checkCacheDir() + { + self::$cacheDir = str_replace('\\', '/', self::$cacheDir); + self::$cacheDir = rtrim(self::$cacheDir, '/') . '/'; + + if (! is_dir(self::$cacheDir)) { + throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir); + } + + if (! is_writable(self::$cacheDir)) { + throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir); + } + } + + /** + * Delete unused cached files + * + * @return void + */ + public static function cleanCache() + { + static $clean = false; + + if ($clean || empty(self::$cacheDir)) { + return; + } + + $clean = true; + + // only remove files with extensions created by SCSSPHP Cache + // css files removed based on the list files + $removeTypes = ['scsscache' => 1]; + + $files = scandir(self::$cacheDir); + + if (! $files) { + return; + } + + $checkTime = time() - self::$gcLifetime; + + foreach ($files as $file) { + // don't delete if the file wasn't created with SCSSPHP Cache + if (strpos($file, self::$prefix) !== 0) { + continue; + } + + $parts = explode('.', $file); + $type = array_pop($parts); + + if (! isset($removeTypes[$type])) { + continue; + } + + $fullPath = self::$cacheDir . $file; + $mtime = filemtime($fullPath); + + // don't delete if it's a relatively new file + if ($mtime > $checkTime) { + continue; + } + + unlink($fullPath); + } + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Colors.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Colors.php new file mode 100644 index 0000000..e836e3f --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Colors.php @@ -0,0 +1,247 @@ + + * + * @internal + */ +class Colors +{ + /** + * CSS Colors + * + * @see http://www.w3.org/TR/css3-color + * + * @var array + */ + protected static $cssColors = [ + 'aliceblue' => '240,248,255', + 'antiquewhite' => '250,235,215', + 'aqua' => '0,255,255', + 'cyan' => '0,255,255', + 'aquamarine' => '127,255,212', + 'azure' => '240,255,255', + 'beige' => '245,245,220', + 'bisque' => '255,228,196', + 'black' => '0,0,0', + 'blanchedalmond' => '255,235,205', + 'blue' => '0,0,255', + 'blueviolet' => '138,43,226', + 'brown' => '165,42,42', + 'burlywood' => '222,184,135', + 'cadetblue' => '95,158,160', + 'chartreuse' => '127,255,0', + 'chocolate' => '210,105,30', + 'coral' => '255,127,80', + 'cornflowerblue' => '100,149,237', + 'cornsilk' => '255,248,220', + 'crimson' => '220,20,60', + 'darkblue' => '0,0,139', + 'darkcyan' => '0,139,139', + 'darkgoldenrod' => '184,134,11', + 'darkgray' => '169,169,169', + 'darkgrey' => '169,169,169', + 'darkgreen' => '0,100,0', + 'darkkhaki' => '189,183,107', + 'darkmagenta' => '139,0,139', + 'darkolivegreen' => '85,107,47', + 'darkorange' => '255,140,0', + 'darkorchid' => '153,50,204', + 'darkred' => '139,0,0', + 'darksalmon' => '233,150,122', + 'darkseagreen' => '143,188,143', + 'darkslateblue' => '72,61,139', + 'darkslategray' => '47,79,79', + 'darkslategrey' => '47,79,79', + 'darkturquoise' => '0,206,209', + 'darkviolet' => '148,0,211', + 'deeppink' => '255,20,147', + 'deepskyblue' => '0,191,255', + 'dimgray' => '105,105,105', + 'dimgrey' => '105,105,105', + 'dodgerblue' => '30,144,255', + 'firebrick' => '178,34,34', + 'floralwhite' => '255,250,240', + 'forestgreen' => '34,139,34', + 'fuchsia' => '255,0,255', + 'magenta' => '255,0,255', + 'gainsboro' => '220,220,220', + 'ghostwhite' => '248,248,255', + 'gold' => '255,215,0', + 'goldenrod' => '218,165,32', + 'gray' => '128,128,128', + 'grey' => '128,128,128', + 'green' => '0,128,0', + 'greenyellow' => '173,255,47', + 'honeydew' => '240,255,240', + 'hotpink' => '255,105,180', + 'indianred' => '205,92,92', + 'indigo' => '75,0,130', + 'ivory' => '255,255,240', + 'khaki' => '240,230,140', + 'lavender' => '230,230,250', + 'lavenderblush' => '255,240,245', + 'lawngreen' => '124,252,0', + 'lemonchiffon' => '255,250,205', + 'lightblue' => '173,216,230', + 'lightcoral' => '240,128,128', + 'lightcyan' => '224,255,255', + 'lightgoldenrodyellow' => '250,250,210', + 'lightgray' => '211,211,211', + 'lightgrey' => '211,211,211', + 'lightgreen' => '144,238,144', + 'lightpink' => '255,182,193', + 'lightsalmon' => '255,160,122', + 'lightseagreen' => '32,178,170', + 'lightskyblue' => '135,206,250', + 'lightslategray' => '119,136,153', + 'lightslategrey' => '119,136,153', + 'lightsteelblue' => '176,196,222', + 'lightyellow' => '255,255,224', + 'lime' => '0,255,0', + 'limegreen' => '50,205,50', + 'linen' => '250,240,230', + 'maroon' => '128,0,0', + 'mediumaquamarine' => '102,205,170', + 'mediumblue' => '0,0,205', + 'mediumorchid' => '186,85,211', + 'mediumpurple' => '147,112,219', + 'mediumseagreen' => '60,179,113', + 'mediumslateblue' => '123,104,238', + 'mediumspringgreen' => '0,250,154', + 'mediumturquoise' => '72,209,204', + 'mediumvioletred' => '199,21,133', + 'midnightblue' => '25,25,112', + 'mintcream' => '245,255,250', + 'mistyrose' => '255,228,225', + 'moccasin' => '255,228,181', + 'navajowhite' => '255,222,173', + 'navy' => '0,0,128', + 'oldlace' => '253,245,230', + 'olive' => '128,128,0', + 'olivedrab' => '107,142,35', + 'orange' => '255,165,0', + 'orangered' => '255,69,0', + 'orchid' => '218,112,214', + 'palegoldenrod' => '238,232,170', + 'palegreen' => '152,251,152', + 'paleturquoise' => '175,238,238', + 'palevioletred' => '219,112,147', + 'papayawhip' => '255,239,213', + 'peachpuff' => '255,218,185', + 'peru' => '205,133,63', + 'pink' => '255,192,203', + 'plum' => '221,160,221', + 'powderblue' => '176,224,230', + 'purple' => '128,0,128', + 'red' => '255,0,0', + 'rosybrown' => '188,143,143', + 'royalblue' => '65,105,225', + 'saddlebrown' => '139,69,19', + 'salmon' => '250,128,114', + 'sandybrown' => '244,164,96', + 'seagreen' => '46,139,87', + 'seashell' => '255,245,238', + 'sienna' => '160,82,45', + 'silver' => '192,192,192', + 'skyblue' => '135,206,235', + 'slateblue' => '106,90,205', + 'slategray' => '112,128,144', + 'slategrey' => '112,128,144', + 'snow' => '255,250,250', + 'springgreen' => '0,255,127', + 'steelblue' => '70,130,180', + 'tan' => '210,180,140', + 'teal' => '0,128,128', + 'thistle' => '216,191,216', + 'tomato' => '255,99,71', + 'turquoise' => '64,224,208', + 'violet' => '238,130,238', + 'wheat' => '245,222,179', + 'white' => '255,255,255', + 'whitesmoke' => '245,245,245', + 'yellow' => '255,255,0', + 'yellowgreen' => '154,205,50', + 'rebeccapurple' => '102,51,153', + 'transparent' => '0,0,0,0', + ]; + + /** + * Convert named color in a [r,g,b[,a]] array + * + * @param string $colorName + * + * @return int[]|null + */ + public static function colorNameToRGBa($colorName) + { + if (\is_string($colorName) && isset(static::$cssColors[$colorName])) { + $rgba = explode(',', static::$cssColors[$colorName]); + + // only case with opacity is transparent, with opacity=0, so we can intval on opacity also + $rgba = array_map('intval', $rgba); + + return $rgba; + } + + return null; + } + + /** + * Reverse conversion : from RGBA to a color name if possible + * + * @param integer $r + * @param integer $g + * @param integer $b + * @param integer|float $a + * + * @return string|null + */ + public static function RGBaToColorName($r, $g, $b, $a = 1) + { + static $reverseColorTable = null; + + if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b) || ! is_numeric($a)) { + return null; + } + + if ($a < 1) { + return null; + } + + if (\is_null($reverseColorTable)) { + $reverseColorTable = []; + + foreach (static::$cssColors as $name => $rgb_str) { + $rgb_str = explode(',', $rgb_str); + + if ( + \count($rgb_str) == 3 && + ! isset($reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])]) + ) { + $reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])] = $name; + } + } + } + + if (isset($reverseColorTable[\intval($r)][\intval($g)][\intval($b)])) { + return $reverseColorTable[\intval($r)][\intval($g)][\intval($b)]; + } + + return null; + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/CompilationResult.php b/user/plugins/admin/vendor/scssphp/scssphp/src/CompilationResult.php new file mode 100644 index 0000000..36adb0d --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/CompilationResult.php @@ -0,0 +1,69 @@ +css = $css; + $this->sourceMap = $sourceMap; + $this->includedFiles = $includedFiles; + } + + /** + * @return string + */ + public function getCss() + { + return $this->css; + } + + /** + * @return string[] + */ + public function getIncludedFiles() + { + return $this->includedFiles; + } + + /** + * The sourceMap content, if it was generated + * + * @return null|string + */ + public function getSourceMap() + { + return $this->sourceMap; + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Compiler.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Compiler.php new file mode 100644 index 0000000..58ba795 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Compiler.php @@ -0,0 +1,10087 @@ + + * + * @final Extending the Compiler is deprecated + */ +class Compiler +{ + /** + * @deprecated + */ + const LINE_COMMENTS = 1; + /** + * @deprecated + */ + const DEBUG_INFO = 2; + + /** + * @deprecated + */ + const WITH_RULE = 1; + /** + * @deprecated + */ + const WITH_MEDIA = 2; + /** + * @deprecated + */ + const WITH_SUPPORTS = 4; + /** + * @deprecated + */ + const WITH_ALL = 7; + + const SOURCE_MAP_NONE = 0; + const SOURCE_MAP_INLINE = 1; + const SOURCE_MAP_FILE = 2; + + /** + * @var array + */ + protected static $operatorNames = [ + '+' => 'add', + '-' => 'sub', + '*' => 'mul', + '/' => 'div', + '%' => 'mod', + + '==' => 'eq', + '!=' => 'neq', + '<' => 'lt', + '>' => 'gt', + + '<=' => 'lte', + '>=' => 'gte', + ]; + + /** + * @var array + */ + protected static $namespaces = [ + 'special' => '%', + 'mixin' => '@', + 'function' => '^', + ]; + + public static $true = [Type::T_KEYWORD, 'true']; + public static $false = [Type::T_KEYWORD, 'false']; + /** @deprecated */ + public static $NaN = [Type::T_KEYWORD, 'NaN']; + /** @deprecated */ + public static $Infinity = [Type::T_KEYWORD, 'Infinity']; + public static $null = [Type::T_NULL]; + public static $nullString = [Type::T_STRING, '', []]; + public static $defaultValue = [Type::T_KEYWORD, '']; + public static $selfSelector = [Type::T_SELF]; + public static $emptyList = [Type::T_LIST, '', []]; + public static $emptyMap = [Type::T_MAP, [], []]; + public static $emptyString = [Type::T_STRING, '"', []]; + public static $with = [Type::T_KEYWORD, 'with']; + public static $without = [Type::T_KEYWORD, 'without']; + + /** + * @var array + */ + protected $importPaths = []; + /** + * @var array + */ + protected $importCache = []; + + /** + * @var string[] + */ + protected $importedFiles = []; + + /** + * @var array + * @phpstan-var array + */ + protected $userFunctions = []; + /** + * @var array + */ + protected $registeredVars = []; + /** + * @var array + */ + protected $registeredFeatures = [ + 'extend-selector-pseudoclass' => false, + 'at-error' => true, + 'units-level-3' => true, + 'global-variable-shadowing' => false, + ]; + + /** + * @var string|null + */ + protected $encoding = null; + /** + * @var null + * @deprecated + */ + protected $lineNumberStyle = null; + + /** + * @var int|SourceMapGenerator + * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator + */ + protected $sourceMap = self::SOURCE_MAP_NONE; + + /** + * @var array + * @phpstan-var array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} + */ + protected $sourceMapOptions = []; + + /** + * @var bool + */ + private $charset = true; + + /** + * @var string|\ScssPhp\ScssPhp\Formatter + */ + protected $formatter = Expanded::class; + + /** + * @var Environment + */ + protected $rootEnv; + /** + * @var OutputBlock|null + */ + protected $rootBlock; + + /** + * @var \ScssPhp\ScssPhp\Compiler\Environment + */ + protected $env; + /** + * @var OutputBlock|null + */ + protected $scope; + /** + * @var Environment|null + */ + protected $storeEnv; + /** + * @var bool|null + * + * @deprecated + */ + protected $charsetSeen; + /** + * @var array + */ + protected $sourceNames; + + /** + * @var Cache|null + */ + protected $cache; + + /** + * @var bool + */ + protected $cacheCheckImportResolutions = false; + + /** + * @var int + */ + protected $indentLevel; + /** + * @var array[] + */ + protected $extends; + /** + * @var array + */ + protected $extendsMap; + + /** + * @var array + */ + protected $parsedFiles = []; + + /** + * @var Parser|null + */ + protected $parser; + /** + * @var int|null + */ + protected $sourceIndex; + /** + * @var int|null + */ + protected $sourceLine; + /** + * @var int|null + */ + protected $sourceColumn; + /** + * @var bool|null + */ + protected $shouldEvaluate; + /** + * @var null + * @deprecated + */ + protected $ignoreErrors; + /** + * @var bool + */ + protected $ignoreCallStackMessage = false; + + /** + * @var array[] + */ + protected $callStack = []; + + /** + * @var array + * @phpstan-var list + */ + private $resolvedImports = []; + + /** + * The directory of the currently processed file + * + * @var string|null + */ + private $currentDirectory; + + /** + * The directory of the input file + * + * @var string + */ + private $rootDirectory; + + /** + * @var bool + */ + private $legacyCwdImportPath = true; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var array + */ + private $warnedChildFunctions = []; + + /** + * Constructor + * + * @param array|null $cacheOptions + * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string, checkImportResolutions?: bool}|null $cacheOptions + */ + public function __construct($cacheOptions = null) + { + $this->sourceNames = []; + + if ($cacheOptions) { + $this->cache = new Cache($cacheOptions); + if (!empty($cacheOptions['checkImportResolutions'])) { + $this->cacheCheckImportResolutions = true; + } + } + + $this->logger = new StreamLogger(fopen('php://stderr', 'w'), true); + } + + /** + * Get compiler options + * + * @return array + * + * @internal + */ + public function getCompileOptions() + { + $options = [ + 'importPaths' => $this->importPaths, + 'registeredVars' => $this->registeredVars, + 'registeredFeatures' => $this->registeredFeatures, + 'encoding' => $this->encoding, + 'sourceMap' => serialize($this->sourceMap), + 'sourceMapOptions' => $this->sourceMapOptions, + 'formatter' => $this->formatter, + 'legacyImportPath' => $this->legacyCwdImportPath, + ]; + + return $options; + } + + /** + * Sets an alternative logger. + * + * Changing the logger in the middle of the compilation is not + * supported and will result in an undefined behavior. + * + * @param LoggerInterface $logger + * + * @return void + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * Set an alternative error output stream, for testing purpose only + * + * @param resource $handle + * + * @return void + * + * @deprecated Use {@see setLogger} instead + */ + public function setErrorOuput($handle) + { + @trigger_error('The method "setErrorOuput" is deprecated. Use "setLogger" instead.', E_USER_DEPRECATED); + + $this->logger = new StreamLogger($handle); + } + + /** + * Compile scss + * + * @param string $code + * @param string|null $path + * + * @return string + * + * @throws SassException when the source fails to compile + * + * @deprecated Use {@see compileString} instead. + */ + public function compile($code, $path = null) + { + @trigger_error(sprintf('The "%s" method is deprecated. Use "compileString" instead.', __METHOD__), E_USER_DEPRECATED); + + $result = $this->compileString($code, $path); + + $sourceMap = $result->getSourceMap(); + + if ($sourceMap !== null) { + if ($this->sourceMap instanceof SourceMapGenerator) { + $this->sourceMap->saveMap($sourceMap); + } elseif ($this->sourceMap === self::SOURCE_MAP_FILE) { + $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); + $sourceMapGenerator->saveMap($sourceMap); + } + } + + return $result->getCss(); + } + + /** + * Compile scss + * + * @param string $source + * @param string|null $path + * + * @return CompilationResult + * + * @throws SassException when the source fails to compile + */ + public function compileString($source, $path = null) + { + if ($this->cache) { + $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($source); + $compileOptions = $this->getCompileOptions(); + $cachedResult = $this->cache->getCache('compile', $cacheKey, $compileOptions); + + if ($cachedResult instanceof CachedResult && $this->isFreshCachedResult($cachedResult)) { + return $cachedResult->getResult(); + } + } + + $this->indentLevel = -1; + $this->extends = []; + $this->extendsMap = []; + $this->sourceIndex = null; + $this->sourceLine = null; + $this->sourceColumn = null; + $this->env = null; + $this->scope = null; + $this->storeEnv = null; + $this->shouldEvaluate = null; + $this->ignoreCallStackMessage = false; + $this->parsedFiles = []; + $this->importedFiles = []; + $this->resolvedImports = []; + + if (!\is_null($path) && is_file($path)) { + $path = realpath($path) ?: $path; + $this->currentDirectory = dirname($path); + $this->rootDirectory = $this->currentDirectory; + } else { + $this->currentDirectory = null; + $this->rootDirectory = getcwd(); + } + + try { + $this->parser = $this->parserFactory($path); + $tree = $this->parser->parse($source); + $this->parser = null; + + $this->formatter = new $this->formatter(); + $this->rootBlock = null; + $this->rootEnv = $this->pushEnv($tree); + + $warnCallback = function ($message, $deprecation) { + $this->logger->warn($message, $deprecation); + }; + $previousWarnCallback = Warn::setCallback($warnCallback); + + try { + $this->injectVariables($this->registeredVars); + $this->compileRoot($tree); + $this->popEnv(); + } finally { + Warn::setCallback($previousWarnCallback); + } + + $sourceMapGenerator = null; + + if ($this->sourceMap) { + if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) { + $sourceMapGenerator = $this->sourceMap; + $this->sourceMap = self::SOURCE_MAP_FILE; + } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) { + $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); + } + } + + $out = $this->formatter->format($this->scope, $sourceMapGenerator); + + $prefix = ''; + + if ($this->charset && strlen($out) !== Util::mbStrlen($out)) { + $prefix = '@charset "UTF-8";' . "\n"; + $out = $prefix . $out; + } + + $sourceMap = null; + + if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) { + $sourceMap = $sourceMapGenerator->generateJson($prefix); + $sourceMapUrl = null; + + switch ($this->sourceMap) { + case self::SOURCE_MAP_INLINE: + $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap)); + break; + + case self::SOURCE_MAP_FILE: + if (isset($this->sourceMapOptions['sourceMapURL'])) { + $sourceMapUrl = $this->sourceMapOptions['sourceMapURL']; + } + break; + } + + if ($sourceMapUrl !== null) { + $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl); + } + } + } catch (SassScriptException $e) { + throw new CompilerException($this->addLocationToMessage($e->getMessage()), 0, $e); + } + + $includedFiles = []; + + foreach ($this->resolvedImports as $resolvedImport) { + $includedFiles[$resolvedImport['filePath']] = $resolvedImport['filePath']; + } + + $result = new CompilationResult($out, $sourceMap, array_values($includedFiles)); + + if ($this->cache && isset($cacheKey) && isset($compileOptions)) { + $this->cache->setCache('compile', $cacheKey, new CachedResult($result, $this->parsedFiles, $this->resolvedImports), $compileOptions); + } + + // Reset state to free memory + // TODO in 2.0, reset parsedFiles as well when the getter is removed. + $this->resolvedImports = []; + $this->importedFiles = []; + + return $result; + } + + /** + * @param CachedResult $result + * + * @return bool + */ + private function isFreshCachedResult(CachedResult $result) + { + // check if any dependency file changed since the result was compiled + foreach ($result->getParsedFiles() as $file => $mtime) { + if (! is_file($file) || filemtime($file) !== $mtime) { + return false; + } + } + + if ($this->cacheCheckImportResolutions) { + $resolvedImports = []; + + foreach ($result->getResolvedImports() as $import) { + $currentDir = $import['currentDir']; + $path = $import['path']; + // store the check across all the results in memory to avoid multiple findImport() on the same path + // with same context. + // this is happening in a same hit with multiple compilations (especially with big frameworks) + if (empty($resolvedImports[$currentDir][$path])) { + $resolvedImports[$currentDir][$path] = $this->findImport($path, $currentDir); + } + + if ($resolvedImports[$currentDir][$path] !== $import['filePath']) { + return false; + } + } + } + + return true; + } + + /** + * Instantiate parser + * + * @param string|null $path + * + * @return \ScssPhp\ScssPhp\Parser + */ + protected function parserFactory($path) + { + // https://sass-lang.com/documentation/at-rules/import + // CSS files imported by Sass don’t allow any special Sass features. + // In order to make sure authors don’t accidentally write Sass in their CSS, + // all Sass features that aren’t also valid CSS will produce errors. + // Otherwise, the CSS will be rendered as-is. It can even be extended! + $cssOnly = false; + + if ($path !== null && substr($path, -4) === '.css') { + $cssOnly = true; + } + + $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly, $this->logger); + + $this->sourceNames[] = $path; + $this->addParsedFile($path); + + return $parser; + } + + /** + * Is self extend? + * + * @param array $target + * @param array $origin + * + * @return boolean + */ + protected function isSelfExtend($target, $origin) + { + foreach ($origin as $sel) { + if (\in_array($target, $sel)) { + return true; + } + } + + return false; + } + + /** + * Push extends + * + * @param array $target + * @param array $origin + * @param array|null $block + * + * @return void + */ + protected function pushExtends($target, $origin, $block) + { + $i = \count($this->extends); + $this->extends[] = [$target, $origin, $block]; + + foreach ($target as $part) { + if (isset($this->extendsMap[$part])) { + $this->extendsMap[$part][] = $i; + } else { + $this->extendsMap[$part] = [$i]; + } + } + } + + /** + * Make output block + * + * @param string|null $type + * @param string[]|null $selectors + * + * @return \ScssPhp\ScssPhp\Formatter\OutputBlock + */ + protected function makeOutputBlock($type, $selectors = null) + { + $out = new OutputBlock(); + $out->type = $type; + $out->lines = []; + $out->children = []; + $out->parent = $this->scope; + $out->selectors = $selectors; + $out->depth = $this->env->depth; + + if ($this->env->block instanceof Block) { + $out->sourceName = $this->env->block->sourceName; + $out->sourceLine = $this->env->block->sourceLine; + $out->sourceColumn = $this->env->block->sourceColumn; + } else { + $out->sourceName = null; + $out->sourceLine = null; + $out->sourceColumn = null; + } + + return $out; + } + + /** + * Compile root + * + * @param \ScssPhp\ScssPhp\Block $rootBlock + * + * @return void + */ + protected function compileRoot(Block $rootBlock) + { + $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT); + + $this->compileChildrenNoReturn($rootBlock->children, $this->scope); + $this->flattenSelectors($this->scope); + $this->missingSelectors(); + } + + /** + * Report missing selectors + * + * @return void + */ + protected function missingSelectors() + { + foreach ($this->extends as $extend) { + if (isset($extend[3])) { + continue; + } + + list($target, $origin, $block) = $extend; + + // ignore if !optional + if ($block[2]) { + continue; + } + + $target = implode(' ', $target); + $origin = $this->collapseSelectors($origin); + + $this->sourceLine = $block[Parser::SOURCE_LINE]; + throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found."); + } + } + + /** + * Flatten selectors + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * @param string $parentKey + * + * @return void + */ + protected function flattenSelectors(OutputBlock $block, $parentKey = null) + { + if ($block->selectors) { + $selectors = []; + + foreach ($block->selectors as $s) { + $selectors[] = $s; + + if (! \is_array($s)) { + continue; + } + + // check extends + if (! empty($this->extendsMap)) { + $this->matchExtends($s, $selectors); + + // remove duplicates + array_walk($selectors, function (&$value) { + $value = serialize($value); + }); + + $selectors = array_unique($selectors); + + array_walk($selectors, function (&$value) { + $value = unserialize($value); + }); + } + } + + $block->selectors = []; + $placeholderSelector = false; + + foreach ($selectors as $selector) { + if ($this->hasSelectorPlaceholder($selector)) { + $placeholderSelector = true; + continue; + } + + $block->selectors[] = $this->compileSelector($selector); + } + + if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) { + unset($block->parent->children[$parentKey]); + + return; + } + } + + foreach ($block->children as $key => $child) { + $this->flattenSelectors($child, $key); + } + } + + /** + * Glue parts of :not( or :nth-child( ... that are in general split in selectors parts + * + * @param array $parts + * + * @return array + */ + protected function glueFunctionSelectors($parts) + { + $new = []; + + foreach ($parts as $part) { + if (\is_array($part)) { + $part = $this->glueFunctionSelectors($part); + $new[] = $part; + } else { + // a selector part finishing with a ) is the last part of a :not( or :nth-child( + // and need to be joined to this + if ( + \count($new) && \is_string($new[\count($new) - 1]) && + \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false + ) { + while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') { + $part = array_pop($new) . $part; + } + $new[\count($new) - 1] .= $part; + } else { + $new[] = $part; + } + } + } + + return $new; + } + + /** + * Match extends + * + * @param array $selector + * @param array $out + * @param integer $from + * @param boolean $initial + * + * @return void + */ + protected function matchExtends($selector, &$out, $from = 0, $initial = true) + { + static $partsPile = []; + $selector = $this->glueFunctionSelectors($selector); + + if (\count($selector) == 1 && \in_array(reset($selector), $partsPile)) { + return; + } + + $outRecurs = []; + + foreach ($selector as $i => $part) { + if ($i < $from) { + continue; + } + + // check that we are not building an infinite loop of extensions + // if the new part is just including a previous part don't try to extend anymore + if (\count($part) > 1) { + foreach ($partsPile as $previousPart) { + if (! \count(array_diff($previousPart, $part))) { + continue 2; + } + } + } + + $partsPile[] = $part; + + if ($this->matchExtendsSingle($part, $origin, $initial)) { + $after = \array_slice($selector, $i + 1); + $before = \array_slice($selector, 0, $i); + list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before); + + foreach ($origin as $new) { + $k = 0; + + // remove shared parts + if (\count($new) > 1) { + while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) { + $k++; + } + } + + if (\count($nonBreakableBefore) && $k === \count($new)) { + $k--; + } + + $replacement = []; + $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new; + + for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) { + $slice = []; + + foreach ($tempReplacement[$l] as $chunk) { + if (! \in_array($chunk, $slice)) { + $slice[] = $chunk; + } + } + + array_unshift($replacement, $slice); + + if (! $this->isImmediateRelationshipCombinator(end($slice))) { + break; + } + } + + $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : []; + + // Merge shared direct relationships. + $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore); + + $result = array_merge( + $before, + $mergedBefore, + $replacement, + $after + ); + + if ($result === $selector) { + continue; + } + + $this->pushOrMergeExtentedSelector($out, $result); + + // recursively check for more matches + $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore)); + + if (\count($origin) > 1) { + $this->matchExtends($result, $out, $startRecurseFrom, false); + } else { + $this->matchExtends($result, $outRecurs, $startRecurseFrom, false); + } + + // selector sequence merging + if (! empty($before) && \count($new) > 1) { + $preSharedParts = $k > 0 ? \array_slice($before, 0, $k) : []; + $postSharedParts = $k > 0 ? \array_slice($before, $k) : $before; + + list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore); + + $result2 = array_merge( + $preSharedParts, + $betweenSharedParts, + $postSharedParts, + $nonBreakabl2, + $nonBreakableBefore, + $replacement, + $after + ); + + $this->pushOrMergeExtentedSelector($out, $result2); + } + } + } + array_pop($partsPile); + } + + while (\count($outRecurs)) { + $result = array_shift($outRecurs); + $this->pushOrMergeExtentedSelector($out, $result); + } + } + + /** + * Test a part for being a pseudo selector + * + * @param string $part + * @param array $matches + * + * @return boolean + */ + protected function isPseudoSelector($part, &$matches) + { + if ( + strpos($part, ':') === 0 && + preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches) + ) { + return true; + } + + return false; + } + + /** + * Push extended selector except if + * - this is a pseudo selector + * - same as previous + * - in a white list + * in this case we merge the pseudo selector content + * + * @param array $out + * @param array $extended + * + * @return void + */ + protected function pushOrMergeExtentedSelector(&$out, $extended) + { + if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) { + $single = reset($extended); + $part = reset($single); + + if ( + $this->isPseudoSelector($part, $matchesExtended) && + \in_array($matchesExtended[1], [ 'slotted' ]) + ) { + $prev = end($out); + $prev = $this->glueFunctionSelectors($prev); + + if (\count($prev) === 1 && \count(reset($prev)) === 1) { + $single = reset($prev); + $part = reset($single); + + if ( + $this->isPseudoSelector($part, $matchesPrev) && + $matchesPrev[1] === $matchesExtended[1] + ) { + $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2); + $extended[1] = $matchesPrev[2] . ', ' . $extended[1]; + $extended = implode($matchesExtended[1] . '(', $extended); + $extended = [ [ $extended ]]; + array_pop($out); + } + } + } + } + $out[] = $extended; + } + + /** + * Match extends single + * + * @param array $rawSingle + * @param array $outOrigin + * @param boolean $initial + * + * @return boolean + */ + protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true) + { + $counts = []; + $single = []; + + // simple usual cases, no need to do the whole trick + if (\in_array($rawSingle, [['>'],['+'],['~']])) { + return false; + } + + foreach ($rawSingle as $part) { + // matches Number + if (! \is_string($part)) { + return false; + } + + if (! preg_match('/^[\[.:#%]/', $part) && \count($single)) { + $single[\count($single) - 1] .= $part; + } else { + $single[] = $part; + } + } + + $extendingDecoratedTag = false; + + if (\count($single) > 1) { + $matches = null; + $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false; + } + + $outOrigin = []; + $found = false; + + foreach ($single as $k => $part) { + if (isset($this->extendsMap[$part])) { + foreach ($this->extendsMap[$part] as $idx) { + $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1; + } + } + + if ( + $initial && + $this->isPseudoSelector($part, $matches) && + ! \in_array($matches[1], [ 'not' ]) + ) { + $buffer = $matches[2]; + $parser = $this->parserFactory(__METHOD__); + + if ($parser->parseSelector($buffer, $subSelectors, false)) { + foreach ($subSelectors as $ksub => $subSelector) { + $subExtended = []; + $this->matchExtends($subSelector, $subExtended, 0, false); + + if ($subExtended) { + $subSelectorsExtended = $subSelectors; + $subSelectorsExtended[$ksub] = $subExtended; + + foreach ($subSelectorsExtended as $ksse => $sse) { + $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse); + } + + $subSelectorsExtended = implode(', ', $subSelectorsExtended); + $singleExtended = $single; + $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part); + $outOrigin[] = [ $singleExtended ]; + $found = true; + } + } + } + } + } + + foreach ($counts as $idx => $count) { + list($target, $origin, /* $block */) = $this->extends[$idx]; + + $origin = $this->glueFunctionSelectors($origin); + + // check count + if ($count !== \count($target)) { + continue; + } + + $this->extends[$idx][3] = true; + + $rem = array_diff($single, $target); + + foreach ($origin as $j => $new) { + // prevent infinite loop when target extends itself + if ($this->isSelfExtend($single, $origin) && ! $initial) { + return false; + } + + $replacement = end($new); + + // Extending a decorated tag with another tag is not possible. + if ( + $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag && + preg_match('/^[a-z0-9]+$/i', $replacement[0]) + ) { + unset($origin[$j]); + continue; + } + + $combined = $this->combineSelectorSingle($replacement, $rem); + + if (\count(array_diff($combined, $origin[$j][\count($origin[$j]) - 1]))) { + $origin[$j][\count($origin[$j]) - 1] = $combined; + } + } + + $outOrigin = array_merge($outOrigin, $origin); + + $found = true; + } + + return $found; + } + + /** + * Extract a relationship from the fragment. + * + * When extracting the last portion of a selector we will be left with a + * fragment which may end with a direction relationship combinator. This + * method will extract the relationship fragment and return it along side + * the rest. + * + * @param array $fragment The selector fragment maybe ending with a direction relationship combinator. + * + * @return array The selector without the relationship fragment if any, the relationship fragment. + */ + protected function extractRelationshipFromFragment(array $fragment) + { + $parents = []; + $children = []; + + $j = $i = \count($fragment); + + for (;;) { + $children = $j != $i ? \array_slice($fragment, $j, $i - $j) : []; + $parents = \array_slice($fragment, 0, $j); + $slice = end($parents); + + if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) { + break; + } + + $j -= 2; + } + + return [$parents, $children]; + } + + /** + * Combine selector single + * + * @param array $base + * @param array $other + * + * @return array + */ + protected function combineSelectorSingle($base, $other) + { + $tag = []; + $out = []; + $wasTag = false; + $pseudo = []; + + while (\count($other) && strpos(end($other), ':') === 0) { + array_unshift($pseudo, array_pop($other)); + } + + foreach ([array_reverse($base), array_reverse($other)] as $single) { + $rang = count($single); + + foreach ($single as $part) { + if (preg_match('/^[\[:]/', $part)) { + $out[] = $part; + $wasTag = false; + } elseif (preg_match('/^[\.#]/', $part)) { + array_unshift($out, $part); + $wasTag = false; + } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) { + $tag[] = $part; + $wasTag = true; + } elseif ($wasTag) { + $tag[\count($tag) - 1] .= $part; + } else { + array_unshift($out, $part); + } + $rang--; + } + } + + if (\count($tag)) { + array_unshift($out, $tag[0]); + } + + while (\count($pseudo)) { + $out[] = array_shift($pseudo); + } + + return $out; + } + + /** + * Compile media + * + * @param \ScssPhp\ScssPhp\Block $media + * + * @return void + */ + protected function compileMedia(Block $media) + { + $this->pushEnv($media); + + $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env)); + + if (! empty($mediaQueries)) { + $previousScope = $this->scope; + $parentScope = $this->mediaParent($this->scope); + + foreach ($mediaQueries as $mediaQuery) { + $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]); + + $parentScope->children[] = $this->scope; + $parentScope = $this->scope; + } + + // top level properties in a media cause it to be wrapped + $needsWrap = false; + + foreach ($media->children as $child) { + $type = $child[0]; + + if ( + $type !== Type::T_BLOCK && + $type !== Type::T_MEDIA && + $type !== Type::T_DIRECTIVE && + $type !== Type::T_IMPORT + ) { + $needsWrap = true; + break; + } + } + + if ($needsWrap) { + $wrapped = new Block(); + $wrapped->sourceName = $media->sourceName; + $wrapped->sourceIndex = $media->sourceIndex; + $wrapped->sourceLine = $media->sourceLine; + $wrapped->sourceColumn = $media->sourceColumn; + $wrapped->selectors = []; + $wrapped->comments = []; + $wrapped->parent = $media; + $wrapped->children = $media->children; + + $media->children = [[Type::T_BLOCK, $wrapped]]; + } + + $this->compileChildrenNoReturn($media->children, $this->scope); + + $this->scope = $previousScope; + } + + $this->popEnv(); + } + + /** + * Media parent + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope + * + * @return \ScssPhp\ScssPhp\Formatter\OutputBlock + */ + protected function mediaParent(OutputBlock $scope) + { + while (! empty($scope->parent)) { + if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) { + break; + } + + $scope = $scope->parent; + } + + return $scope; + } + + /** + * Compile directive + * + * @param \ScssPhp\ScssPhp\Block|array $directive + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * + * @return void + */ + protected function compileDirective($directive, OutputBlock $out) + { + if (\is_array($directive)) { + $directiveName = $this->compileDirectiveName($directive[0]); + $s = '@' . $directiveName; + + if (! empty($directive[1])) { + $s .= ' ' . $this->compileValue($directive[1]); + } + // sass-spec compliance on newline after directives, a bit tricky :/ + $appendNewLine = (! empty($directive[2]) || strpos($s, "\n")) ? "\n" : ""; + if (\is_array($directive[0]) && empty($directive[1])) { + $appendNewLine = "\n"; + } + + if (empty($directive[3])) { + $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type::T_COMMENT, Type::T_DIRECTIVE]); + } else { + $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';'); + } + } else { + $directive->name = $this->compileDirectiveName($directive->name); + $s = '@' . $directive->name; + + if (! empty($directive->value)) { + $s .= ' ' . $this->compileValue($directive->value); + } + + if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') { + $this->compileKeyframeBlock($directive, [$s]); + } else { + $this->compileNestedBlock($directive, [$s]); + } + } + } + + /** + * directive names can include some interpolation + * + * @param string|array $directiveName + * @return string + * @throws CompilerException + */ + protected function compileDirectiveName($directiveName) + { + if (is_string($directiveName)) { + return $directiveName; + } + + return $this->compileValue($directiveName); + } + + /** + * Compile at-root + * + * @param \ScssPhp\ScssPhp\Block $block + * + * @return void + */ + protected function compileAtRoot(Block $block) + { + $env = $this->pushEnv($block); + $envs = $this->compactEnv($env); + list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null); + + // wrap inline selector + if ($block->selector) { + $wrapped = new Block(); + $wrapped->sourceName = $block->sourceName; + $wrapped->sourceIndex = $block->sourceIndex; + $wrapped->sourceLine = $block->sourceLine; + $wrapped->sourceColumn = $block->sourceColumn; + $wrapped->selectors = $block->selector; + $wrapped->comments = []; + $wrapped->parent = $block; + $wrapped->children = $block->children; + $wrapped->selfParent = $block->selfParent; + + $block->children = [[Type::T_BLOCK, $wrapped]]; + $block->selector = null; + } + + $selfParent = $block->selfParent; + assert($selfParent !== null, 'at-root blocks must have a selfParent set.'); + + if ( + ! $selfParent->selectors && + isset($block->parent) && $block->parent && + isset($block->parent->selectors) && $block->parent->selectors + ) { + $selfParent = $block->parent; + } + + $this->env = $this->filterWithWithout($envs, $with, $without); + + $saveScope = $this->scope; + $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without); + + // propagate selfParent to the children where they still can be useful + $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent); + + $this->scope = $this->completeScope($this->scope, $saveScope); + $this->scope = $saveScope; + $this->env = $this->extractEnv($envs); + + $this->popEnv(); + } + + /** + * Filter at-root scope depending of with/without option + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope + * @param array $with + * @param array $without + * + * @return OutputBlock + */ + protected function filterScopeWithWithout($scope, $with, $without) + { + $filteredScopes = []; + $childStash = []; + + if ($scope->type === Type::T_ROOT) { + return $scope; + } + + // start from the root + while ($scope->parent && $scope->parent->type !== Type::T_ROOT) { + array_unshift($childStash, $scope); + $scope = $scope->parent; + } + + for (;;) { + if (! $scope) { + break; + } + + if ($this->isWith($scope, $with, $without)) { + $s = clone $scope; + $s->children = []; + $s->lines = []; + $s->parent = null; + + if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) { + $s->selectors = []; + } + + $filteredScopes[] = $s; + } + + if (\count($childStash)) { + $scope = array_shift($childStash); + } elseif ($scope->children) { + $scope = end($scope->children); + } else { + $scope = null; + } + } + + if (! \count($filteredScopes)) { + return $this->rootBlock; + } + + $newScope = array_shift($filteredScopes); + $newScope->parent = $this->rootBlock; + + $this->rootBlock->children[] = $newScope; + + $p = &$newScope; + + while (\count($filteredScopes)) { + $s = array_shift($filteredScopes); + $s->parent = $p; + $p->children[] = $s; + $newScope = &$p->children[0]; + $p = &$p->children[0]; + } + + return $newScope; + } + + /** + * found missing selector from a at-root compilation in the previous scope + * (if at-root is just enclosing a property, the selector is in the parent tree) + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope + * + * @return OutputBlock + */ + protected function completeScope($scope, $previousScope) + { + if (! $scope->type && (! $scope->selectors || ! \count($scope->selectors)) && \count($scope->lines)) { + $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth); + } + + if ($scope->children) { + foreach ($scope->children as $k => $c) { + $scope->children[$k] = $this->completeScope($c, $previousScope); + } + } + + return $scope; + } + + /** + * Find a selector by the depth node in the scope + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope + * @param integer $depth + * + * @return array + */ + protected function findScopeSelectors($scope, $depth) + { + if ($scope->depth === $depth && $scope->selectors) { + return $scope->selectors; + } + + if ($scope->children) { + foreach (array_reverse($scope->children) as $c) { + if ($s = $this->findScopeSelectors($c, $depth)) { + return $s; + } + } + } + + return []; + } + + /** + * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later + * + * @param array $withCondition + * + * @return array + */ + protected function compileWith($withCondition) + { + // just compile what we have in 2 lists + $with = []; + $without = ['rule' => true]; + + if ($withCondition) { + if ($withCondition[0] === Type::T_INTERPOLATE) { + $w = $this->compileValue($withCondition); + + $buffer = "($w)"; + $parser = $this->parserFactory(__METHOD__); + + if ($parser->parseValue($buffer, $reParsedWith)) { + $withCondition = $reParsedWith; + } + } + + if ($this->mapHasKey($withCondition, static::$with)) { + $without = []; // cancel the default + $list = $this->coerceList($this->libMapGet([$withCondition, static::$with])); + + foreach ($list[2] as $item) { + $keyword = $this->compileStringContent($this->coerceString($item)); + + $with[$keyword] = true; + } + } + + if ($this->mapHasKey($withCondition, static::$without)) { + $without = []; // cancel the default + $list = $this->coerceList($this->libMapGet([$withCondition, static::$without])); + + foreach ($list[2] as $item) { + $keyword = $this->compileStringContent($this->coerceString($item)); + + $without[$keyword] = true; + } + } + } + + return [$with, $without]; + } + + /** + * Filter env stack + * + * @param Environment[] $envs + * @param array $with + * @param array $without + * + * @return Environment + * + * @phpstan-param non-empty-array $envs + */ + protected function filterWithWithout($envs, $with, $without) + { + $filtered = []; + + foreach ($envs as $e) { + if ($e->block && ! $this->isWith($e->block, $with, $without)) { + $ec = clone $e; + $ec->block = null; + $ec->selectors = []; + + $filtered[] = $ec; + } else { + $filtered[] = $e; + } + } + + return $this->extractEnv($filtered); + } + + /** + * Filter WITH rules + * + * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block + * @param array $with + * @param array $without + * + * @return boolean + */ + protected function isWith($block, $with, $without) + { + if (isset($block->type)) { + if ($block->type === Type::T_MEDIA) { + return $this->testWithWithout('media', $with, $without); + } + + if ($block->type === Type::T_DIRECTIVE) { + if (isset($block->name)) { + return $this->testWithWithout($this->compileDirectiveName($block->name), $with, $without); + } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) { + return $this->testWithWithout($m[1], $with, $without); + } else { + return $this->testWithWithout('???', $with, $without); + } + } + } elseif (isset($block->selectors)) { + // a selector starting with number is a keyframe rule + if (\count($block->selectors)) { + $s = reset($block->selectors); + + while (\is_array($s)) { + $s = reset($s); + } + + if (\is_object($s) && $s instanceof Number) { + return $this->testWithWithout('keyframes', $with, $without); + } + } + + return $this->testWithWithout('rule', $with, $without); + } + + return true; + } + + /** + * Test a single type of block against with/without lists + * + * @param string $what + * @param array $with + * @param array $without + * + * @return boolean + * true if the block should be kept, false to reject + */ + protected function testWithWithout($what, $with, $without) + { + // if without, reject only if in the list (or 'all' is in the list) + if (\count($without)) { + return (isset($without[$what]) || isset($without['all'])) ? false : true; + } + + // otherwise reject all what is not in the with list + return (isset($with[$what]) || isset($with['all'])) ? true : false; + } + + + /** + * Compile keyframe block + * + * @param \ScssPhp\ScssPhp\Block $block + * @param string[] $selectors + * + * @return void + */ + protected function compileKeyframeBlock(Block $block, $selectors) + { + $env = $this->pushEnv($block); + + $envs = $this->compactEnv($env); + + $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) { + return ! isset($e->block->selectors); + })); + + $this->scope = $this->makeOutputBlock($block->type, $selectors); + $this->scope->depth = 1; + $this->scope->parent->children[] = $this->scope; + + $this->compileChildrenNoReturn($block->children, $this->scope); + + $this->scope = $this->scope->parent; + $this->env = $this->extractEnv($envs); + + $this->popEnv(); + } + + /** + * Compile nested properties lines + * + * @param \ScssPhp\ScssPhp\Block $block + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * + * @return void + */ + protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out) + { + $prefix = $this->compileValue($block->prefix) . '-'; + + $nested = $this->makeOutputBlock($block->type); + $nested->parent = $out; + + if ($block->hasValue) { + $nested->depth = $out->depth + 1; + } + + $out->children[] = $nested; + + foreach ($block->children as $child) { + switch ($child[0]) { + case Type::T_ASSIGN: + array_unshift($child[1][2], $prefix); + break; + + case Type::T_NESTED_PROPERTY: + array_unshift($child[1]->prefix[2], $prefix); + break; + } + + $this->compileChild($child, $nested); + } + } + + /** + * Compile nested block + * + * @param \ScssPhp\ScssPhp\Block $block + * @param string[] $selectors + * + * @return void + */ + protected function compileNestedBlock(Block $block, $selectors) + { + $this->pushEnv($block); + + $this->scope = $this->makeOutputBlock($block->type, $selectors); + $this->scope->parent->children[] = $this->scope; + + // wrap assign children in a block + // except for @font-face + if ($block->type !== Type::T_DIRECTIVE || $this->compileDirectiveName($block->name) !== 'font-face') { + // need wrapping? + $needWrapping = false; + + foreach ($block->children as $child) { + if ($child[0] === Type::T_ASSIGN) { + $needWrapping = true; + break; + } + } + + if ($needWrapping) { + $wrapped = new Block(); + $wrapped->sourceName = $block->sourceName; + $wrapped->sourceIndex = $block->sourceIndex; + $wrapped->sourceLine = $block->sourceLine; + $wrapped->sourceColumn = $block->sourceColumn; + $wrapped->selectors = []; + $wrapped->comments = []; + $wrapped->parent = $block; + $wrapped->children = $block->children; + $wrapped->selfParent = $block->selfParent; + + $block->children = [[Type::T_BLOCK, $wrapped]]; + } + } + + $this->compileChildrenNoReturn($block->children, $this->scope); + + $this->scope = $this->scope->parent; + + $this->popEnv(); + } + + /** + * Recursively compiles a block. + * + * A block is analogous to a CSS block in most cases. A single SCSS document + * is encapsulated in a block when parsed, but it does not have parent tags + * so all of its children appear on the root level when compiled. + * + * Blocks are made up of selectors and children. + * + * The children of a block are just all the blocks that are defined within. + * + * Compiling the block involves pushing a fresh environment on the stack, + * and iterating through the props, compiling each one. + * + * @see Compiler::compileChild() + * + * @param \ScssPhp\ScssPhp\Block $block + * + * @return void + */ + protected function compileBlock(Block $block) + { + $env = $this->pushEnv($block); + $env->selectors = $this->evalSelectors($block->selectors); + + $out = $this->makeOutputBlock(null); + + $this->scope->children[] = $out; + + if (\count($block->children)) { + $out->selectors = $this->multiplySelectors($env, $block->selfParent); + + // propagate selfParent to the children where they still can be useful + $selfParentSelectors = null; + + if (isset($block->selfParent->selectors)) { + $selfParentSelectors = $block->selfParent->selectors; + $block->selfParent->selectors = $out->selectors; + } + + $this->compileChildrenNoReturn($block->children, $out, $block->selfParent); + + // and revert for the following children of the same block + if ($selfParentSelectors) { + $block->selfParent->selectors = $selfParentSelectors; + } + } + + $this->popEnv(); + } + + + /** + * Compile the value of a comment that can have interpolation + * + * @param array $value + * @param boolean $pushEnv + * + * @return string + */ + protected function compileCommentValue($value, $pushEnv = false) + { + $c = $value[1]; + + if (isset($value[2])) { + if ($pushEnv) { + $this->pushEnv(); + } + + try { + $c = $this->compileValue($value[2]); + } catch (SassScriptException $e) { + $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $this->addLocationToMessage($e->getMessage()), true); + // ignore error in comment compilation which are only interpolation + } catch (SassException $e) { + $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $e->getMessage(), true); + // ignore error in comment compilation which are only interpolation + } + + if ($pushEnv) { + $this->popEnv(); + } + } + + return $c; + } + + /** + * Compile root level comment + * + * @param array $block + * + * @return void + */ + protected function compileComment($block) + { + $out = $this->makeOutputBlock(Type::T_COMMENT); + $out->lines[] = $this->compileCommentValue($block, true); + + $this->scope->children[] = $out; + } + + /** + * Evaluate selectors + * + * @param array $selectors + * + * @return array + */ + protected function evalSelectors($selectors) + { + $this->shouldEvaluate = false; + + $selectors = array_map([$this, 'evalSelector'], $selectors); + + // after evaluating interpolates, we might need a second pass + if ($this->shouldEvaluate) { + $selectors = $this->replaceSelfSelector($selectors, '&'); + $buffer = $this->collapseSelectors($selectors); + $parser = $this->parserFactory(__METHOD__); + + try { + $isValid = $parser->parseSelector($buffer, $newSelectors, true); + } catch (ParserException $e) { + throw $this->error($e->getMessage()); + } + + if ($isValid) { + $selectors = array_map([$this, 'evalSelector'], $newSelectors); + } + } + + return $selectors; + } + + /** + * Evaluate selector + * + * @param array $selector + * + * @return array + */ + protected function evalSelector($selector) + { + return array_map([$this, 'evalSelectorPart'], $selector); + } + + /** + * Evaluate selector part; replaces all the interpolates, stripping quotes + * + * @param array $part + * + * @return array + */ + protected function evalSelectorPart($part) + { + foreach ($part as &$p) { + if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) { + $p = $this->compileValue($p); + + // force re-evaluation if self char or non standard char + if (preg_match(',[^\w-],', $p)) { + $this->shouldEvaluate = true; + } + } elseif ( + \is_string($p) && \strlen($p) >= 2 && + ($first = $p[0]) && ($first === '"' || $first === "'") && + substr($p, -1) === $first + ) { + $p = substr($p, 1, -1); + } + } + + return $this->flattenSelectorSingle($part); + } + + /** + * Collapse selectors + * + * @param array $selectors + * + * @return string + */ + protected function collapseSelectors($selectors) + { + $parts = []; + + foreach ($selectors as $selector) { + $output = []; + + foreach ($selector as $node) { + $compound = ''; + + array_walk_recursive( + $node, + function ($value, $key) use (&$compound) { + $compound .= $value; + } + ); + + $output[] = $compound; + } + + $parts[] = implode(' ', $output); + } + + return implode(', ', $parts); + } + + /** + * Collapse selectors + * + * @param array $selectors + * + * @return array + */ + private function collapseSelectorsAsList($selectors) + { + $parts = []; + + foreach ($selectors as $selector) { + $output = []; + $glueNext = false; + + foreach ($selector as $node) { + $compound = ''; + + array_walk_recursive( + $node, + function ($value, $key) use (&$compound) { + $compound .= $value; + } + ); + + if ($this->isImmediateRelationshipCombinator($compound)) { + if (\count($output)) { + $output[\count($output) - 1] .= ' ' . $compound; + } else { + $output[] = $compound; + } + + $glueNext = true; + } elseif ($glueNext) { + $output[\count($output) - 1] .= ' ' . $compound; + $glueNext = false; + } else { + $output[] = $compound; + } + } + + foreach ($output as &$o) { + $o = [Type::T_STRING, '', [$o]]; + } + + $parts[] = [Type::T_LIST, ' ', $output]; + } + + return [Type::T_LIST, ',', $parts]; + } + + /** + * Parse down the selector and revert [self] to "&" before a reparsing + * + * @param array $selectors + * @param string|null $replace + * + * @return array + */ + protected function replaceSelfSelector($selectors, $replace = null) + { + foreach ($selectors as &$part) { + if (\is_array($part)) { + if ($part === [Type::T_SELF]) { + if (\is_null($replace)) { + $replace = $this->reduce([Type::T_SELF]); + $replace = $this->compileValue($replace); + } + $part = $replace; + } else { + $part = $this->replaceSelfSelector($part, $replace); + } + } + } + + return $selectors; + } + + /** + * Flatten selector single; joins together .classes and #ids + * + * @param array $single + * + * @return array + */ + protected function flattenSelectorSingle($single) + { + $joined = []; + + foreach ($single as $part) { + if ( + empty($joined) || + ! \is_string($part) || + preg_match('/[\[.:#%]/', $part) + ) { + $joined[] = $part; + continue; + } + + if (\is_array(end($joined))) { + $joined[] = $part; + } else { + $joined[\count($joined) - 1] .= $part; + } + } + + return $joined; + } + + /** + * Compile selector to string; self(&) should have been replaced by now + * + * @param string|array $selector + * + * @return string + */ + protected function compileSelector($selector) + { + if (! \is_array($selector)) { + return $selector; // media and the like + } + + return implode( + ' ', + array_map( + [$this, 'compileSelectorPart'], + $selector + ) + ); + } + + /** + * Compile selector part + * + * @param array $piece + * + * @return string + */ + protected function compileSelectorPart($piece) + { + foreach ($piece as &$p) { + if (! \is_array($p)) { + continue; + } + + switch ($p[0]) { + case Type::T_SELF: + $p = '&'; + break; + + default: + $p = $this->compileValue($p); + break; + } + } + + return implode($piece); + } + + /** + * Has selector placeholder? + * + * @param array $selector + * + * @return boolean + */ + protected function hasSelectorPlaceholder($selector) + { + if (! \is_array($selector)) { + return false; + } + + foreach ($selector as $parts) { + foreach ($parts as $part) { + if (\strlen($part) && '%' === $part[0]) { + return true; + } + } + } + + return false; + } + + /** + * @param string $name + * + * @return void + */ + protected function pushCallStack($name = '') + { + $this->callStack[] = [ + 'n' => $name, + Parser::SOURCE_INDEX => $this->sourceIndex, + Parser::SOURCE_LINE => $this->sourceLine, + Parser::SOURCE_COLUMN => $this->sourceColumn + ]; + + // infinite calling loop + if (\count($this->callStack) > 25000) { + // not displayed but you can var_dump it to deep debug + $msg = $this->callStackMessage(true, 100); + $msg = 'Infinite calling loop'; + + throw $this->error($msg); + } + } + + /** + * @return void + */ + protected function popCallStack() + { + array_pop($this->callStack); + } + + /** + * Compile children and return result + * + * @param array $stms + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param string $traceName + * + * @return array|Number|null + */ + protected function compileChildren($stms, OutputBlock $out, $traceName = '') + { + $this->pushCallStack($traceName); + + foreach ($stms as $stm) { + $ret = $this->compileChild($stm, $out); + + if (isset($ret)) { + $this->popCallStack(); + + return $ret; + } + } + + $this->popCallStack(); + + return null; + } + + /** + * Compile children and throw exception if unexpected `@return` + * + * @param array $stms + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param \ScssPhp\ScssPhp\Block $selfParent + * @param string $traceName + * + * @return void + * + * @throws \Exception + */ + protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '') + { + $this->pushCallStack($traceName); + + foreach ($stms as $stm) { + if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) { + $stm[1]->selfParent = $selfParent; + $ret = $this->compileChild($stm, $out); + $stm[1]->selfParent = null; + } elseif ($selfParent && \in_array($stm[0], [Type::T_INCLUDE, Type::T_EXTEND])) { + $stm['selfParent'] = $selfParent; + $ret = $this->compileChild($stm, $out); + unset($stm['selfParent']); + } else { + $ret = $this->compileChild($stm, $out); + } + + if (isset($ret)) { + throw $this->error('@return may only be used within a function'); + } + } + + $this->popCallStack(); + } + + + /** + * evaluate media query : compile internal value keeping the structure unchanged + * + * @param array $queryList + * + * @return array + */ + protected function evaluateMediaQuery($queryList) + { + static $parser = null; + + $outQueryList = []; + + foreach ($queryList as $kql => $query) { + $shouldReparse = false; + + foreach ($query as $kq => $q) { + for ($i = 1; $i < \count($q); $i++) { + $value = $this->compileValue($q[$i]); + + // the parser had no mean to know if media type or expression if it was an interpolation + // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type + if ( + $q[0] == Type::T_MEDIA_TYPE && + (strpos($value, '(') !== false || + strpos($value, ')') !== false || + strpos($value, ':') !== false || + strpos($value, ',') !== false) + ) { + $shouldReparse = true; + } + + $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value]; + } + } + + if ($shouldReparse) { + if (\is_null($parser)) { + $parser = $this->parserFactory(__METHOD__); + } + + $queryString = $this->compileMediaQuery([$queryList[$kql]]); + $queryString = reset($queryString); + + if (strpos($queryString, '@media ') === 0) { + $queryString = substr($queryString, 7); + $queries = []; + + if ($parser->parseMediaQueryList($queryString, $queries)) { + $queries = $this->evaluateMediaQuery($queries[2]); + + while (\count($queries)) { + $outQueryList[] = array_shift($queries); + } + + continue; + } + } + } + + $outQueryList[] = $queryList[$kql]; + } + + return $outQueryList; + } + + /** + * Compile media query + * + * @param array $queryList + * + * @return string[] + */ + protected function compileMediaQuery($queryList) + { + $start = '@media '; + $default = trim($start); + $out = []; + $current = ''; + + foreach ($queryList as $query) { + $type = null; + $parts = []; + + $mediaTypeOnly = true; + + foreach ($query as $q) { + if ($q[0] !== Type::T_MEDIA_TYPE) { + $mediaTypeOnly = false; + break; + } + } + + foreach ($query as $q) { + switch ($q[0]) { + case Type::T_MEDIA_TYPE: + $newType = array_map([$this, 'compileValue'], \array_slice($q, 1)); + + // combining not and anything else than media type is too risky and should be avoided + if (! $mediaTypeOnly) { + if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, $type) )) { + if ($type) { + array_unshift($parts, implode(' ', array_filter($type))); + } + + if (! empty($parts)) { + if (\strlen($current)) { + $current .= $this->formatter->tagSeparator; + } + + $current .= implode(' and ', $parts); + } + + if ($current) { + $out[] = $start . $current; + } + + $current = ''; + $type = null; + $parts = []; + } + } + + if ($newType === ['all'] && $default) { + $default = $start . 'all'; + } + + // all can be safely ignored and mixed with whatever else + if ($newType !== ['all']) { + if ($type) { + $type = $this->mergeMediaTypes($type, $newType); + + if (empty($type)) { + // merge failed : ignore this query that is not valid, skip to the next one + $parts = []; + $default = ''; // if everything fail, no @media at all + continue 3; + } + } else { + $type = $newType; + } + } + break; + + case Type::T_MEDIA_EXPRESSION: + if (isset($q[2])) { + $parts[] = '(' + . $this->compileValue($q[1]) + . $this->formatter->assignSeparator + . $this->compileValue($q[2]) + . ')'; + } else { + $parts[] = '(' + . $this->compileValue($q[1]) + . ')'; + } + break; + + case Type::T_MEDIA_VALUE: + $parts[] = $this->compileValue($q[1]); + break; + } + } + + if ($type) { + array_unshift($parts, implode(' ', array_filter($type))); + } + + if (! empty($parts)) { + if (\strlen($current)) { + $current .= $this->formatter->tagSeparator; + } + + $current .= implode(' and ', $parts); + } + } + + if ($current) { + $out[] = $start . $current; + } + + // no @media type except all, and no conflict? + if (! $out && $default) { + $out[] = $default; + } + + return $out; + } + + /** + * Merge direct relationships between selectors + * + * @param array $selectors1 + * @param array $selectors2 + * + * @return array + */ + protected function mergeDirectRelationships($selectors1, $selectors2) + { + if (empty($selectors1) || empty($selectors2)) { + return array_merge($selectors1, $selectors2); + } + + $part1 = end($selectors1); + $part2 = end($selectors2); + + if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) { + return array_merge($selectors1, $selectors2); + } + + $merged = []; + + do { + $part1 = array_pop($selectors1); + $part2 = array_pop($selectors2); + + if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) { + if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) { + array_unshift($merged, [$part1[0] . $part2[0]]); + $merged = array_merge($selectors1, $selectors2, $merged); + } else { + $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged); + } + + break; + } + + array_unshift($merged, $part1); + } while (! empty($selectors1) && ! empty($selectors2)); + + return $merged; + } + + /** + * Merge media types + * + * @param array $type1 + * @param array $type2 + * + * @return array|null + */ + protected function mergeMediaTypes($type1, $type2) + { + if (empty($type1)) { + return $type2; + } + + if (empty($type2)) { + return $type1; + } + + if (\count($type1) > 1) { + $m1 = strtolower($type1[0]); + $t1 = strtolower($type1[1]); + } else { + $m1 = ''; + $t1 = strtolower($type1[0]); + } + + if (\count($type2) > 1) { + $m2 = strtolower($type2[0]); + $t2 = strtolower($type2[1]); + } else { + $m2 = ''; + $t2 = strtolower($type2[0]); + } + + if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) { + if ($t1 === $t2) { + return null; + } + + return [ + $m1 === Type::T_NOT ? $m2 : $m1, + $m1 === Type::T_NOT ? $t2 : $t1, + ]; + } + + if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) { + // CSS has no way of representing "neither screen nor print" + if ($t1 !== $t2) { + return null; + } + + return [Type::T_NOT, $t1]; + } + + if ($t1 !== $t2) { + return null; + } + + // t1 == t2, neither m1 nor m2 are "not" + return [empty($m1) ? $m2 : $m1, $t1]; + } + + /** + * Compile import; returns true if the value was something that could be imported + * + * @param array $rawPath + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param boolean $once + * + * @return boolean + */ + protected function compileImport($rawPath, OutputBlock $out, $once = false) + { + if ($rawPath[0] === Type::T_STRING) { + $path = $this->compileStringContent($rawPath); + + if (strpos($path, 'url(') !== 0 && $filePath = $this->findImport($path, $this->currentDirectory)) { + $this->registerImport($this->currentDirectory, $path, $filePath); + + if (! $once || ! \in_array($filePath, $this->importedFiles)) { + $this->importFile($filePath, $out); + $this->importedFiles[] = $filePath; + } + + return true; + } + + $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); + + return false; + } + + if ($rawPath[0] === Type::T_LIST) { + // handle a list of strings + if (\count($rawPath[2]) === 0) { + return false; + } + + foreach ($rawPath[2] as $path) { + if ($path[0] !== Type::T_STRING) { + $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); + + return false; + } + } + + foreach ($rawPath[2] as $path) { + $this->compileImport($path, $out, $once); + } + + return true; + } + + $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); + + return false; + } + + /** + * @param array $rawPath + * @return string + * @throws CompilerException + */ + protected function compileImportPath($rawPath) + { + $path = $this->compileValue($rawPath); + + // case url() without quotes : suppress \r \n remaining in the path + // if this is a real string there can not be CR or LF char + if (strpos($path, 'url(') === 0) { + $path = str_replace(array("\r", "\n"), array('', ' '), $path); + } else { + // if this is a file name in a string, spaces should be escaped + $path = $this->reduce($rawPath); + $path = $this->escapeImportPathString($path); + $path = $this->compileValue($path); + } + + return $path; + } + + /** + * @param array $path + * @return array + * @throws CompilerException + */ + protected function escapeImportPathString($path) + { + switch ($path[0]) { + case Type::T_LIST: + foreach ($path[2] as $k => $v) { + $path[2][$k] = $this->escapeImportPathString($v); + } + break; + case Type::T_STRING: + if ($path[1]) { + $path = $this->compileValue($path); + $path = str_replace(' ', '\\ ', $path); + $path = [Type::T_KEYWORD, $path]; + } + break; + } + + return $path; + } + + /** + * Append a root directive like @import or @charset as near as the possible from the source code + * (keeping before comments, @import and @charset coming before in the source code) + * + * @param string $line + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param array $allowed + * + * @return void + */ + protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT]) + { + $root = $out; + + while ($root->parent) { + $root = $root->parent; + } + + $i = 0; + + while ($i < \count($root->children)) { + if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) { + break; + } + + $i++; + } + + // remove incompatible children from the bottom of the list + $saveChildren = []; + + while ($i < \count($root->children)) { + $saveChildren[] = array_pop($root->children); + } + + // insert the directive as a comment + $child = $this->makeOutputBlock(Type::T_COMMENT); + $child->lines[] = $line; + $child->sourceName = $this->sourceNames[$this->sourceIndex]; + $child->sourceLine = $this->sourceLine; + $child->sourceColumn = $this->sourceColumn; + + $root->children[] = $child; + + // repush children + while (\count($saveChildren)) { + $root->children[] = array_pop($saveChildren); + } + } + + /** + * Append lines to the current output block: + * directly to the block or through a child if necessary + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param string $type + * @param string $line + * + * @return void + */ + protected function appendOutputLine(OutputBlock $out, $type, $line) + { + $outWrite = &$out; + + // check if it's a flat output or not + if (\count($out->children)) { + $lastChild = &$out->children[\count($out->children) - 1]; + + if ( + $lastChild->depth === $out->depth && + \is_null($lastChild->selectors) && + ! \count($lastChild->children) + ) { + $outWrite = $lastChild; + } else { + $nextLines = $this->makeOutputBlock($type); + $nextLines->parent = $out; + $nextLines->depth = $out->depth; + + $out->children[] = $nextLines; + $outWrite = &$nextLines; + } + } + + $outWrite->lines[] = $line; + } + + /** + * Compile child; returns a value to halt execution + * + * @param array $child + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * + * @return array|Number|null + */ + protected function compileChild($child, OutputBlock $out) + { + if (isset($child[Parser::SOURCE_LINE])) { + $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null; + $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1; + $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1; + } elseif (\is_array($child) && isset($child[1]->sourceLine)) { + $this->sourceIndex = $child[1]->sourceIndex; + $this->sourceLine = $child[1]->sourceLine; + $this->sourceColumn = $child[1]->sourceColumn; + } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) { + $this->sourceLine = $out->sourceLine; + $sourceIndex = array_search($out->sourceName, $this->sourceNames); + $this->sourceColumn = $out->sourceColumn; + + if ($sourceIndex === false) { + $sourceIndex = null; + } + $this->sourceIndex = $sourceIndex; + } + + switch ($child[0]) { + case Type::T_SCSSPHP_IMPORT_ONCE: + $rawPath = $this->reduce($child[1]); + + $this->compileImport($rawPath, $out, true); + break; + + case Type::T_IMPORT: + $rawPath = $this->reduce($child[1]); + + $this->compileImport($rawPath, $out); + break; + + case Type::T_DIRECTIVE: + $this->compileDirective($child[1], $out); + break; + + case Type::T_AT_ROOT: + $this->compileAtRoot($child[1]); + break; + + case Type::T_MEDIA: + $this->compileMedia($child[1]); + break; + + case Type::T_BLOCK: + $this->compileBlock($child[1]); + break; + + case Type::T_CHARSET: + break; + + case Type::T_CUSTOM_PROPERTY: + list(, $name, $value) = $child; + $compiledName = $this->compileValue($name); + + // if the value reduces to null from something else then + // the property should be discarded + if ($value[0] !== Type::T_NULL) { + $value = $this->reduce($value); + + if ($value[0] === Type::T_NULL || $value === static::$nullString) { + break; + } + } + + $compiledValue = $this->compileValue($value); + + $line = $this->formatter->customProperty( + $compiledName, + $compiledValue + ); + + $this->appendOutputLine($out, Type::T_ASSIGN, $line); + break; + + case Type::T_ASSIGN: + list(, $name, $value) = $child; + + if ($name[0] === Type::T_VARIABLE) { + $flags = isset($child[3]) ? $child[3] : []; + $isDefault = \in_array('!default', $flags); + $isGlobal = \in_array('!global', $flags); + + if ($isGlobal) { + $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value); + break; + } + + $shouldSet = $isDefault && + (\is_null($result = $this->get($name[1], false)) || + $result === static::$null); + + if (! $isDefault || $shouldSet) { + $this->set($name[1], $this->reduce($value), true, null, $value); + } + break; + } + + $compiledName = $this->compileValue($name); + + // handle shorthand syntaxes : size / line-height... + if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) { + if ($value[0] === Type::T_VARIABLE) { + // if the font value comes from variable, the content is already reduced + // (i.e., formulas were already calculated), so we need the original unreduced value + $value = $this->get($value[1], true, null, true); + } + + $shorthandValue=&$value; + + $shorthandDividerNeedsUnit = false; + $maxListElements = null; + $maxShorthandDividers = 1; + + switch ($compiledName) { + case 'border-radius': + $maxListElements = 4; + $shorthandDividerNeedsUnit = true; + break; + } + + if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') { + // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica" + // we need to handle the first list element + $shorthandValue=&$value[2][0]; + } + + if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') { + $revert = true; + + if ($shorthandDividerNeedsUnit) { + $divider = $shorthandValue[3]; + + if (\is_array($divider)) { + $divider = $this->reduce($divider, true); + } + + if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) { + $revert = false; + } + } + + if ($revert) { + $shorthandValue = $this->expToString($shorthandValue); + } + } elseif ($shorthandValue[0] === Type::T_LIST) { + foreach ($shorthandValue[2] as &$item) { + if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') { + if ($maxShorthandDividers > 0) { + $revert = true; + + // if the list of values is too long, this has to be a shorthand, + // otherwise it could be a real division + if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) { + if ($shorthandDividerNeedsUnit) { + $divider = $item[3]; + + if (\is_array($divider)) { + $divider = $this->reduce($divider, true); + } + + if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) { + $revert = false; + } + } + } + + if ($revert) { + $item = $this->expToString($item); + $maxShorthandDividers--; + } + } + } + } + } + } + + // if the value reduces to null from something else then + // the property should be discarded + if ($value[0] !== Type::T_NULL) { + $value = $this->reduce($value); + + if ($value[0] === Type::T_NULL || $value === static::$nullString) { + break; + } + } + + $compiledValue = $this->compileValue($value); + + // ignore empty value + if (\strlen($compiledValue)) { + $line = $this->formatter->property( + $compiledName, + $compiledValue + ); + $this->appendOutputLine($out, Type::T_ASSIGN, $line); + } + break; + + case Type::T_COMMENT: + if ($out->type === Type::T_ROOT) { + $this->compileComment($child); + break; + } + + $line = $this->compileCommentValue($child, true); + $this->appendOutputLine($out, Type::T_COMMENT, $line); + break; + + case Type::T_MIXIN: + case Type::T_FUNCTION: + list(, $block) = $child; + // the block need to be able to go up to it's parent env to resolve vars + $block->parentEnv = $this->getStoreEnv(); + $this->set(static::$namespaces[$block->type] . $block->name, $block, true); + break; + + case Type::T_EXTEND: + foreach ($child[1] as $sel) { + $replacedSel = $this->replaceSelfSelector($sel); + + if ($replacedSel !== $sel) { + throw $this->error('Parent selectors aren\'t allowed here.'); + } + + $results = $this->evalSelectors([$sel]); + + foreach ($results as $result) { + if (\count($result) !== 1) { + throw $this->error('complex selectors may not be extended.'); + } + + // only use the first one + $result = $result[0]; + $selectors = $out->selectors; + + if (! $selectors && isset($child['selfParent'])) { + $selectors = $this->multiplySelectors($this->env, $child['selfParent']); + } + + if (\count($result) > 1) { + $replacement = implode(', ', $result); + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + + $message = <<logger->warn($message); + } + + $this->pushExtends($result, $selectors, $child); + } + } + break; + + case Type::T_IF: + list(, $if) = $child; + + if ($this->isTruthy($this->reduce($if->cond, true))) { + return $this->compileChildren($if->children, $out); + } + + foreach ($if->cases as $case) { + if ( + $case->type === Type::T_ELSE || + $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond)) + ) { + return $this->compileChildren($case->children, $out); + } + } + break; + + case Type::T_EACH: + list(, $each) = $child; + + $list = $this->coerceList($this->reduce($each->list), ',', true); + + $this->pushEnv(); + + foreach ($list[2] as $item) { + if (\count($each->vars) === 1) { + $this->set($each->vars[0], $item, true); + } else { + list(,, $values) = $this->coerceList($item); + + foreach ($each->vars as $i => $var) { + $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true); + } + } + + $ret = $this->compileChildren($each->children, $out); + + if ($ret) { + $store = $this->env->store; + $this->popEnv(); + $this->backPropagateEnv($store, $each->vars); + + return $ret; + } + } + $store = $this->env->store; + $this->popEnv(); + $this->backPropagateEnv($store, $each->vars); + + break; + + case Type::T_WHILE: + list(, $while) = $child; + + while ($this->isTruthy($this->reduce($while->cond, true))) { + $ret = $this->compileChildren($while->children, $out); + + if ($ret) { + return $ret; + } + } + break; + + case Type::T_FOR: + list(, $for) = $child; + + $startNumber = $this->assertNumber($this->reduce($for->start, true)); + $endNumber = $this->assertNumber($this->reduce($for->end, true)); + + $start = $this->assertInteger($startNumber); + + $numeratorUnits = $startNumber->getNumeratorUnits(); + $denominatorUnits = $startNumber->getDenominatorUnits(); + + $end = $this->assertInteger($endNumber->coerce($numeratorUnits, $denominatorUnits)); + + $d = $start < $end ? 1 : -1; + + $this->pushEnv(); + + for (;;) { + if ( + (! $for->until && $start - $d == $end) || + ($for->until && $start == $end) + ) { + break; + } + + $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits)); + $start += $d; + + $ret = $this->compileChildren($for->children, $out); + + if ($ret) { + $store = $this->env->store; + $this->popEnv(); + $this->backPropagateEnv($store, [$for->var]); + + return $ret; + } + } + + $store = $this->env->store; + $this->popEnv(); + $this->backPropagateEnv($store, [$for->var]); + + break; + + case Type::T_RETURN: + return $this->reduce($child[1], true); + + case Type::T_NESTED_PROPERTY: + $this->compileNestedPropertiesBlock($child[1], $out); + break; + + case Type::T_INCLUDE: + // including a mixin + list(, $name, $argValues, $content, $argUsing) = $child; + + $mixin = $this->get(static::$namespaces['mixin'] . $name, false); + + if (! $mixin) { + throw $this->error("Undefined mixin $name"); + } + + $callingScope = $this->getStoreEnv(); + + // push scope, apply args + $this->pushEnv(); + $this->env->depth--; + + // Find the parent selectors in the env to be able to know what '&' refers to in the mixin + // and assign this fake parent to childs + $selfParent = null; + + if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) { + $selfParent = $child['selfParent']; + } else { + $parentSelectors = $this->multiplySelectors($this->env); + + if ($parentSelectors) { + $parent = new Block(); + $parent->selectors = $parentSelectors; + + foreach ($mixin->children as $k => $child) { + if (isset($child[1]) && \is_object($child[1]) && $child[1] instanceof Block) { + $mixin->children[$k][1]->parent = $parent; + } + } + } + } + + // clone the stored content to not have its scope spoiled by a further call to the same mixin + // i.e., recursive @include of the same mixin + if (isset($content)) { + $copyContent = clone $content; + $copyContent->scope = clone $callingScope; + + $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env); + } else { + $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env); + } + + // save the "using" argument list for applying it to when "@content" is invoked + if (isset($argUsing)) { + $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env); + } else { + $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env); + } + + if (isset($mixin->args)) { + $this->applyArguments($mixin->args, $argValues); + } + + $this->env->marker = 'mixin'; + + if (! empty($mixin->parentEnv)) { + $this->env->declarationScopeParent = $mixin->parentEnv; + } else { + throw $this->error("@mixin $name() without parentEnv"); + } + + $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name); + + $this->popEnv(); + break; + + case Type::T_MIXIN_CONTENT: + $env = isset($this->storeEnv) ? $this->storeEnv : $this->env; + $content = $this->get(static::$namespaces['special'] . 'content', false, $env); + $argUsing = $this->get(static::$namespaces['special'] . 'using', false, $env); + $argContent = $child[1]; + + if (! $content) { + break; + } + + $storeEnv = $this->storeEnv; + $varsUsing = []; + + if (isset($argUsing) && isset($argContent)) { + // Get the arguments provided for the content with the names provided in the "using" argument list + $this->storeEnv = null; + $varsUsing = $this->applyArguments($argUsing, $argContent, false); + } + + // restore the scope from the @content + $this->storeEnv = $content->scope; + + // append the vars from using if any + foreach ($varsUsing as $name => $val) { + $this->set($name, $val, true, $this->storeEnv); + } + + $this->compileChildrenNoReturn($content->children, $out); + + $this->storeEnv = $storeEnv; + break; + + case Type::T_DEBUG: + list(, $value) = $child; + + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + $value = $this->compileDebugValue($value); + + $this->logger->debug("$fname:$line DEBUG: $value"); + break; + + case Type::T_WARN: + list(, $value) = $child; + + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + $value = $this->compileDebugValue($value); + + $this->logger->warn("$value\n on line $line of $fname"); + break; + + case Type::T_ERROR: + list(, $value) = $child; + + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + $value = $this->compileValue($this->reduce($value, true)); + + throw $this->error("File $fname on line $line ERROR: $value\n"); + + default: + throw $this->error("unknown child type: $child[0]"); + } + } + + /** + * Reduce expression to string + * + * @param array $exp + * @param bool $keepParens + * + * @return array + */ + protected function expToString($exp, $keepParens = false) + { + list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp; + + $content = []; + + if ($keepParens && $inParens) { + $content[] = '('; + } + + $content[] = $this->reduce($left); + + if ($whiteLeft) { + $content[] = ' '; + } + + $content[] = $op; + + if ($whiteRight) { + $content[] = ' '; + } + + $content[] = $this->reduce($right); + + if ($keepParens && $inParens) { + $content[] = ')'; + } + + return [Type::T_STRING, '', $content]; + } + + /** + * Is truthy? + * + * @param array|Number $value + * + * @return boolean + */ + public function isTruthy($value) + { + return $value !== static::$false && $value !== static::$null; + } + + /** + * Is the value a direct relationship combinator? + * + * @param string $value + * + * @return boolean + */ + protected function isImmediateRelationshipCombinator($value) + { + return $value === '>' || $value === '+' || $value === '~'; + } + + /** + * Should $value cause its operand to eval + * + * @param array $value + * + * @return boolean + */ + protected function shouldEval($value) + { + switch ($value[0]) { + case Type::T_EXPRESSION: + if ($value[1] === '/') { + return $this->shouldEval($value[2]) || $this->shouldEval($value[3]); + } + + // fall-thru + case Type::T_VARIABLE: + case Type::T_FUNCTION_CALL: + return true; + } + + return false; + } + + /** + * Reduce value + * + * @param array|Number $value + * @param boolean $inExp + * + * @return array|Number + */ + protected function reduce($value, $inExp = false) + { + if ($value instanceof Number) { + return $value; + } + + switch ($value[0]) { + case Type::T_EXPRESSION: + list(, $op, $left, $right, $inParens) = $value; + + $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op; + $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right); + + $left = $this->reduce($left, true); + + if ($op !== 'and' && $op !== 'or') { + $right = $this->reduce($right, true); + } + + // special case: looks like css shorthand + if ( + $opName == 'div' && ! $inParens && ! $inExp && + (($right[0] !== Type::T_NUMBER && isset($right[2]) && $right[2] != '') || + ($right[0] === Type::T_NUMBER && ! $right->unitless())) + ) { + return $this->expToString($value); + } + + $left = $this->coerceForExpression($left); + $right = $this->coerceForExpression($right); + $ltype = $left[0]; + $rtype = $right[0]; + + $ucOpName = ucfirst($opName); + $ucLType = ucfirst($ltype); + $ucRType = ucfirst($rtype); + + // this tries: + // 1. op[op name][left type][right type] + // 2. op[left type][right type] (passing the op as first arg + // 3. op[op name] + $fn = "op${ucOpName}${ucLType}${ucRType}"; + + if ( + \is_callable([$this, $fn]) || + (($fn = "op${ucLType}${ucRType}") && + \is_callable([$this, $fn]) && + $passOp = true) || + (($fn = "op${ucOpName}") && + \is_callable([$this, $fn]) && + $genOp = true) + ) { + $shouldEval = $inParens || $inExp; + + if (isset($passOp)) { + $out = $this->$fn($op, $left, $right, $shouldEval); + } else { + $out = $this->$fn($left, $right, $shouldEval); + } + + if (isset($out)) { + return $out; + } + } + + return $this->expToString($value); + + case Type::T_UNARY: + list(, $op, $exp, $inParens) = $value; + + $inExp = $inExp || $this->shouldEval($exp); + $exp = $this->reduce($exp); + + if ($exp instanceof Number) { + switch ($op) { + case '+': + return $exp; + + case '-': + return $exp->unaryMinus(); + } + } + + if ($op === 'not') { + if ($inExp || $inParens) { + if ($exp === static::$false || $exp === static::$null) { + return static::$true; + } + + return static::$false; + } + + $op = $op . ' '; + } + + return [Type::T_STRING, '', [$op, $exp]]; + + case Type::T_VARIABLE: + return $this->reduce($this->get($value[1])); + + case Type::T_LIST: + foreach ($value[2] as &$item) { + $item = $this->reduce($item); + } + unset($item); + + if (isset($value[3]) && \is_array($value[3])) { + foreach ($value[3] as &$item) { + $item = $this->reduce($item); + } + unset($item); + } + + return $value; + + case Type::T_MAP: + foreach ($value[1] as &$item) { + $item = $this->reduce($item); + } + + foreach ($value[2] as &$item) { + $item = $this->reduce($item); + } + + return $value; + + case Type::T_STRING: + foreach ($value[2] as &$item) { + if (\is_array($item) || $item instanceof Number) { + $item = $this->reduce($item); + } + } + + return $value; + + case Type::T_INTERPOLATE: + $value[1] = $this->reduce($value[1]); + + if ($inExp) { + return [Type::T_KEYWORD, $this->compileValue($value, false)]; + } + + return $value; + + case Type::T_FUNCTION_CALL: + return $this->fncall($value[1], $value[2]); + + case Type::T_SELF: + $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null; + $selfSelector = $this->multiplySelectors($this->env, $selfParent); + $selfSelector = $this->collapseSelectorsAsList($selfSelector); + + return $selfSelector; + + default: + return $value; + } + } + + /** + * Function caller + * + * @param string|array $functionReference + * @param array $argValues + * + * @return array|Number + */ + protected function fncall($functionReference, $argValues) + { + // a string means this is a static hard reference coming from the parsing + if (is_string($functionReference)) { + $name = $functionReference; + + $functionReference = $this->getFunctionReference($name); + if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { + $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]]; + } + } + + // a function type means we just want a plain css function call + if ($functionReference[0] === Type::T_FUNCTION) { + // for CSS functions, simply flatten the arguments into a list + $listArgs = []; + + foreach ((array) $argValues as $arg) { + if (empty($arg[0]) || count($argValues) === 1) { + $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1])); + } + } + + return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]]; + } + + if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { + return static::$defaultValue; + } + + + switch ($functionReference[1]) { + // SCSS @function + case 'scss': + return $this->callScssFunction($functionReference[3], $argValues); + + // native PHP functions + case 'user': + case 'native': + list(,,$name, $fn, $prototype) = $functionReference; + + // special cases of css valid functions min/max + $name = strtolower($name); + if (\in_array($name, ['min', 'max']) && count($argValues) >= 1) { + $cssFunction = $this->cssValidArg( + [Type::T_FUNCTION_CALL, $name, $argValues], + ['min', 'max', 'calc', 'env', 'var'] + ); + if ($cssFunction !== false) { + return $cssFunction; + } + } + $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues); + + if (! isset($returnValue)) { + return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues); + } + + return $returnValue; + + default: + return static::$defaultValue; + } + } + + /** + * @param array|Number $arg + * @param string[] $allowed_function + * @param bool $inFunction + * + * @return array|Number|false + */ + protected function cssValidArg($arg, $allowed_function = [], $inFunction = false) + { + if ($arg instanceof Number) { + return $this->stringifyFncallArgs($arg); + } + + switch ($arg[0]) { + case Type::T_INTERPOLATE: + return [Type::T_KEYWORD, $this->CompileValue($arg)]; + + case Type::T_FUNCTION: + if (! \in_array($arg[1], $allowed_function)) { + return false; + } + if ($arg[2][0] === Type::T_LIST) { + foreach ($arg[2][2] as $k => $subarg) { + $arg[2][2][$k] = $this->cssValidArg($subarg, $allowed_function, $arg[1]); + if ($arg[2][2][$k] === false) { + return false; + } + } + } + return $arg; + + case Type::T_FUNCTION_CALL: + if (! \in_array($arg[1], $allowed_function)) { + return false; + } + $cssArgs = []; + foreach ($arg[2] as $argValue) { + if ($argValue === static::$null) { + return false; + } + $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]); + if (empty($argValue[0]) && $cssArg !== false) { + $cssArgs[] = [$argValue[0], $cssArg]; + } else { + return false; + } + } + + return $this->fncall([Type::T_FUNCTION, $arg[1], [Type::T_LIST, ',', []]], $cssArgs); + + case Type::T_STRING: + case Type::T_KEYWORD: + if (!$inFunction or !\in_array($inFunction, ['calc', 'env', 'var'])) { + return false; + } + return $this->stringifyFncallArgs($arg); + + case Type::T_LIST: + if (!$inFunction) { + return false; + } + if (empty($arg['enclosing']) and $arg[1] === '') { + foreach ($arg[2] as $k => $subarg) { + $arg[2][$k] = $this->cssValidArg($subarg, $allowed_function, $inFunction); + if ($arg[2][$k] === false) { + return false; + } + } + $arg[0] = Type::T_STRING; + return $arg; + } + return false; + + case Type::T_EXPRESSION: + if (! \in_array($arg[1], ['+', '-', '/', '*'])) { + return false; + } + $arg[2] = $this->cssValidArg($arg[2], $allowed_function, $inFunction); + $arg[3] = $this->cssValidArg($arg[3], $allowed_function, $inFunction); + if ($arg[2] === false || $arg[3] === false) { + return false; + } + return $this->expToString($arg, true); + + case Type::T_VARIABLE: + case Type::T_SELF: + default: + return false; + } + } + + + /** + * Reformat fncall arguments to proper css function output + * + * @param array|Number $arg + * + * @return array|Number + */ + protected function stringifyFncallArgs($arg) + { + if ($arg instanceof Number) { + return $arg; + } + + switch ($arg[0]) { + case Type::T_LIST: + foreach ($arg[2] as $k => $v) { + $arg[2][$k] = $this->stringifyFncallArgs($v); + } + break; + + case Type::T_EXPRESSION: + if ($arg[1] === '/') { + $arg[2] = $this->stringifyFncallArgs($arg[2]); + $arg[3] = $this->stringifyFncallArgs($arg[3]); + $arg[5] = $arg[6] = false; // no space around / + $arg = $this->expToString($arg); + } + break; + + case Type::T_FUNCTION_CALL: + $name = strtolower($arg[1]); + + if (in_array($name, ['max', 'min', 'calc'])) { + $args = $arg[2]; + $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args); + } + break; + } + + return $arg; + } + + /** + * Find a function reference + * @param string $name + * @param bool $safeCopy + * @return array + */ + protected function getFunctionReference($name, $safeCopy = false) + { + // SCSS @function + if ($func = $this->get(static::$namespaces['function'] . $name, false)) { + if ($safeCopy) { + $func = clone $func; + } + + return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func]; + } + + // native PHP functions + + // try to find a native lib function + $normalizedName = $this->normalizeName($name); + + if (isset($this->userFunctions[$normalizedName])) { + // see if we can find a user function + list($f, $prototype) = $this->userFunctions[$normalizedName]; + + return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype]; + } + + $lowercasedName = strtolower($normalizedName); + + // Special functions overriding a CSS function are case-insensitive. We normalize them as lowercase + // to avoid the deprecation warning about the wrong case being used. + if ($lowercasedName === 'min' || $lowercasedName === 'max') { + $normalizedName = $lowercasedName; + } + + if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) { + $libName = $f[1]; + $prototype = isset(static::$$libName) ? static::$$libName : null; + + // All core functions have a prototype defined. Not finding the + // prototype can mean 2 things: + // - the function comes from a child class (deprecated just after) + // - the function was found with a different case, which relates to calling the + // wrong Sass function due to our camelCase usage (`fade-in()` vs `fadein()`), + // because PHP method names are case-insensitive while property names are + // case-sensitive. + if ($prototype === null || strtolower($normalizedName) !== $normalizedName) { + $r = new \ReflectionMethod($this, $libName); + $actualLibName = $r->name; + + if ($actualLibName !== $libName || strtolower($normalizedName) !== $normalizedName) { + $kebabCaseName = preg_replace('~(?<=\\w)([A-Z])~', '-$1', substr($actualLibName, 3)); + assert($kebabCaseName !== null); + $originalName = strtolower($kebabCaseName); + $warning = "Calling built-in functions with a non-standard name is deprecated since Scssphp 1.8.0 and will not work anymore in 2.0 (they will be treated as CSS function calls instead).\nUse \"$originalName\" instead of \"$name\"."; + @trigger_error($warning, E_USER_DEPRECATED); + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + Warn::deprecation("$warning\n on line $line of $fname"); + + // Use the actual function definition + $prototype = isset(static::$$actualLibName) ? static::$$actualLibName : null; + $f[1] = $libName = $actualLibName; + } + } + + if (\get_class($this) !== __CLASS__ && !isset($this->warnedChildFunctions[$libName])) { + $r = new \ReflectionMethod($this, $libName); + $declaringClass = $r->getDeclaringClass()->name; + + $needsWarning = $this->warnedChildFunctions[$libName] = $declaringClass !== __CLASS__; + + if ($needsWarning) { + if (method_exists(__CLASS__, $libName)) { + @trigger_error(sprintf('Overriding the "%s" core function by extending the Compiler is deprecated and will be unsupported in 2.0. Remove the "%s::%s" method.', $normalizedName, $declaringClass, $libName), E_USER_DEPRECATED); + } else { + @trigger_error(sprintf('Registering custom functions by extending the Compiler and using the lib* discovery mechanism is deprecated and will be removed in 2.0. Replace the "%s::%s" method with registering the "%s" function through "Compiler::registerFunction".', $declaringClass, $libName, $normalizedName), E_USER_DEPRECATED); + } + } + } + + return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype]; + } + + return static::$null; + } + + + /** + * Normalize name + * + * @param string $name + * + * @return string + */ + protected function normalizeName($name) + { + return str_replace('-', '_', $name); + } + + /** + * Normalize value + * + * @internal + * + * @param array|Number $value + * + * @return array|Number + */ + public function normalizeValue($value) + { + $value = $this->coerceForExpression($this->reduce($value)); + + if ($value instanceof Number) { + return $value; + } + + switch ($value[0]) { + case Type::T_LIST: + $value = $this->extractInterpolation($value); + + if ($value[0] !== Type::T_LIST) { + return [Type::T_KEYWORD, $this->compileValue($value)]; + } + + foreach ($value[2] as $key => $item) { + $value[2][$key] = $this->normalizeValue($item); + } + + if (! empty($value['enclosing'])) { + unset($value['enclosing']); + } + + return $value; + + case Type::T_STRING: + return [$value[0], '"', [$this->compileStringContent($value)]]; + + case Type::T_INTERPOLATE: + return [Type::T_KEYWORD, $this->compileValue($value)]; + + default: + return $value; + } + } + + /** + * Add numbers + * + * @param Number $left + * @param Number $right + * + * @return Number + */ + protected function opAddNumberNumber(Number $left, Number $right) + { + return $left->plus($right); + } + + /** + * Multiply numbers + * + * @param Number $left + * @param Number $right + * + * @return Number + */ + protected function opMulNumberNumber(Number $left, Number $right) + { + return $left->times($right); + } + + /** + * Subtract numbers + * + * @param Number $left + * @param Number $right + * + * @return Number + */ + protected function opSubNumberNumber(Number $left, Number $right) + { + return $left->minus($right); + } + + /** + * Divide numbers + * + * @param Number $left + * @param Number $right + * + * @return Number + */ + protected function opDivNumberNumber(Number $left, Number $right) + { + return $left->dividedBy($right); + } + + /** + * Mod numbers + * + * @param Number $left + * @param Number $right + * + * @return Number + */ + protected function opModNumberNumber(Number $left, Number $right) + { + return $left->modulo($right); + } + + /** + * Add strings + * + * @param array $left + * @param array $right + * + * @return array|null + */ + protected function opAdd($left, $right) + { + if ($strLeft = $this->coerceString($left)) { + if ($right[0] === Type::T_STRING) { + $right[1] = ''; + } + + $strLeft[2][] = $right; + + return $strLeft; + } + + if ($strRight = $this->coerceString($right)) { + if ($left[0] === Type::T_STRING) { + $left[1] = ''; + } + + array_unshift($strRight[2], $left); + + return $strRight; + } + + return null; + } + + /** + * Boolean and + * + * @param array|Number $left + * @param array|Number $right + * @param boolean $shouldEval + * + * @return array|Number|null + */ + protected function opAnd($left, $right, $shouldEval) + { + $truthy = ($left === static::$null || $right === static::$null) || + ($left === static::$false || $left === static::$true) && + ($right === static::$false || $right === static::$true); + + if (! $shouldEval) { + if (! $truthy) { + return null; + } + } + + if ($left !== static::$false && $left !== static::$null) { + return $this->reduce($right, true); + } + + return $left; + } + + /** + * Boolean or + * + * @param array|Number $left + * @param array|Number $right + * @param boolean $shouldEval + * + * @return array|Number|null + */ + protected function opOr($left, $right, $shouldEval) + { + $truthy = ($left === static::$null || $right === static::$null) || + ($left === static::$false || $left === static::$true) && + ($right === static::$false || $right === static::$true); + + if (! $shouldEval) { + if (! $truthy) { + return null; + } + } + + if ($left !== static::$false && $left !== static::$null) { + return $left; + } + + return $this->reduce($right, true); + } + + /** + * Compare colors + * + * @param string $op + * @param array $left + * @param array $right + * + * @return array + */ + protected function opColorColor($op, $left, $right) + { + if ($op !== '==' && $op !== '!=') { + $warning = "Color arithmetic is deprecated and will be an error in future versions.\n" + . "Consider using Sass's color functions instead."; + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + + Warn::deprecation("$warning\n on line $line of $fname"); + } + + $out = [Type::T_COLOR]; + + foreach ([1, 2, 3] as $i) { + $lval = isset($left[$i]) ? $left[$i] : 0; + $rval = isset($right[$i]) ? $right[$i] : 0; + + switch ($op) { + case '+': + $out[] = $lval + $rval; + break; + + case '-': + $out[] = $lval - $rval; + break; + + case '*': + $out[] = $lval * $rval; + break; + + case '%': + if ($rval == 0) { + throw $this->error("color: Can't take modulo by zero"); + } + + $out[] = $lval % $rval; + break; + + case '/': + if ($rval == 0) { + throw $this->error("color: Can't divide by zero"); + } + + $out[] = (int) ($lval / $rval); + break; + + case '==': + return $this->opEq($left, $right); + + case '!=': + return $this->opNeq($left, $right); + + default: + throw $this->error("color: unknown op $op"); + } + } + + if (isset($left[4])) { + $out[4] = $left[4]; + } elseif (isset($right[4])) { + $out[4] = $right[4]; + } + + return $this->fixColor($out); + } + + /** + * Compare color and number + * + * @param string $op + * @param array $left + * @param Number $right + * + * @return array + */ + protected function opColorNumber($op, $left, Number $right) + { + if ($op === '==') { + return static::$false; + } + + if ($op === '!=') { + return static::$true; + } + + $value = $right->getDimension(); + + return $this->opColorColor( + $op, + $left, + [Type::T_COLOR, $value, $value, $value] + ); + } + + /** + * Compare number and color + * + * @param string $op + * @param Number $left + * @param array $right + * + * @return array + */ + protected function opNumberColor($op, Number $left, $right) + { + if ($op === '==') { + return static::$false; + } + + if ($op === '!=') { + return static::$true; + } + + $value = $left->getDimension(); + + return $this->opColorColor( + $op, + [Type::T_COLOR, $value, $value, $value], + $right + ); + } + + /** + * Compare number1 == number2 + * + * @param array|Number $left + * @param array|Number $right + * + * @return array + */ + protected function opEq($left, $right) + { + if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { + $lStr[1] = ''; + $rStr[1] = ''; + + $left = $this->compileValue($lStr); + $right = $this->compileValue($rStr); + } + + return $this->toBool($left === $right); + } + + /** + * Compare number1 != number2 + * + * @param array|Number $left + * @param array|Number $right + * + * @return array + */ + protected function opNeq($left, $right) + { + if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { + $lStr[1] = ''; + $rStr[1] = ''; + + $left = $this->compileValue($lStr); + $right = $this->compileValue($rStr); + } + + return $this->toBool($left !== $right); + } + + /** + * Compare number1 == number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opEqNumberNumber(Number $left, Number $right) + { + return $this->toBool($left->equals($right)); + } + + /** + * Compare number1 != number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opNeqNumberNumber(Number $left, Number $right) + { + return $this->toBool(!$left->equals($right)); + } + + /** + * Compare number1 >= number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opGteNumberNumber(Number $left, Number $right) + { + return $this->toBool($left->greaterThanOrEqual($right)); + } + + /** + * Compare number1 > number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opGtNumberNumber(Number $left, Number $right) + { + return $this->toBool($left->greaterThan($right)); + } + + /** + * Compare number1 <= number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opLteNumberNumber(Number $left, Number $right) + { + return $this->toBool($left->lessThanOrEqual($right)); + } + + /** + * Compare number1 < number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opLtNumberNumber(Number $left, Number $right) + { + return $this->toBool($left->lessThan($right)); + } + + /** + * Cast to boolean + * + * @api + * + * @param bool $thing + * + * @return array + */ + public function toBool($thing) + { + return $thing ? static::$true : static::$false; + } + + /** + * Escape non printable chars in strings output as in dart-sass + * + * @internal + * + * @param string $string + * @param bool $inKeyword + * + * @return string + */ + public function escapeNonPrintableChars($string, $inKeyword = false) + { + static $replacement = []; + if (empty($replacement[$inKeyword])) { + for ($i = 0; $i < 32; $i++) { + if ($i !== 9 || $inKeyword) { + $replacement[$inKeyword][chr($i)] = '\\' . dechex($i) . ($inKeyword ? ' ' : chr(0)); + } + } + } + $string = str_replace(array_keys($replacement[$inKeyword]), array_values($replacement[$inKeyword]), $string); + // chr(0) is not a possible char from the input, so any chr(0) comes from our escaping replacement + if (strpos($string, chr(0)) !== false) { + if (substr($string, -1) === chr(0)) { + $string = substr($string, 0, -1); + } + $string = str_replace( + [chr(0) . '\\',chr(0) . ' '], + [ '\\', ' '], + $string + ); + if (strpos($string, chr(0)) !== false) { + $parts = explode(chr(0), $string); + $string = array_shift($parts); + while (count($parts)) { + $next = array_shift($parts); + if (strpos("0123456789abcdefABCDEF" . chr(9), $next[0]) !== false) { + $string .= " "; + } + $string .= $next; + } + } + } + + return $string; + } + + /** + * Compiles a primitive value into a CSS property value. + * + * Values in scssphp are typed by being wrapped in arrays, their format is + * typically: + * + * array(type, contents [, additional_contents]*) + * + * The input is expected to be reduced. This function will not work on + * things like expressions and variables. + * + * @api + * + * @param array|Number $value + * @param bool $quote + * + * @return string + */ + public function compileValue($value, $quote = true) + { + $value = $this->reduce($value); + + if ($value instanceof Number) { + return $value->output($this); + } + + switch ($value[0]) { + case Type::T_KEYWORD: + return $this->escapeNonPrintableChars($value[1], true); + + case Type::T_COLOR: + // [1] - red component (either number for a %) + // [2] - green component + // [3] - blue component + // [4] - optional alpha component + list(, $r, $g, $b) = $value; + + $r = $this->compileRGBAValue($r); + $g = $this->compileRGBAValue($g); + $b = $this->compileRGBAValue($b); + + if (\count($value) === 5) { + $alpha = $this->compileRGBAValue($value[4], true); + + if (! is_numeric($alpha) || $alpha < 1) { + $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha); + + if (! \is_null($colorName)) { + return $colorName; + } + + if (is_numeric($alpha)) { + $a = new Number($alpha, ''); + } else { + $a = $alpha; + } + + return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')'; + } + } + + if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) { + return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')'; + } + + $colorName = Colors::RGBaToColorName($r, $g, $b); + + if (! \is_null($colorName)) { + return $colorName; + } + + $h = sprintf('#%02x%02x%02x', $r, $g, $b); + + // Converting hex color to short notation (e.g. #003399 to #039) + if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { + $h = '#' . $h[1] . $h[3] . $h[5]; + } + + return $h; + + case Type::T_STRING: + $content = $this->compileStringContent($value, $quote); + + if ($value[1] && $quote) { + $content = str_replace('\\', '\\\\', $content); + + $content = $this->escapeNonPrintableChars($content); + + // force double quote as string quote for the output in certain cases + if ( + $value[1] === "'" && + (strpos($content, '"') === false or strpos($content, "'") !== false) && + strpbrk($content, '{}\\\'') !== false + ) { + $value[1] = '"'; + } elseif ( + $value[1] === '"' && + (strpos($content, '"') !== false and strpos($content, "'") === false) + ) { + $value[1] = "'"; + } + + $content = str_replace($value[1], '\\' . $value[1], $content); + } + + return $value[1] . $content . $value[1]; + + case Type::T_FUNCTION: + $args = ! empty($value[2]) ? $this->compileValue($value[2], $quote) : ''; + + return "$value[1]($args)"; + + case Type::T_FUNCTION_REFERENCE: + $name = ! empty($value[2]) ? $value[2] : ''; + + return "get-function(\"$name\")"; + + case Type::T_LIST: + $value = $this->extractInterpolation($value); + + if ($value[0] !== Type::T_LIST) { + return $this->compileValue($value, $quote); + } + + list(, $delim, $items) = $value; + $pre = $post = ''; + + if (! empty($value['enclosing'])) { + switch ($value['enclosing']) { + case 'parent': + //$pre = '('; + //$post = ')'; + break; + case 'forced_parent': + $pre = '('; + $post = ')'; + break; + case 'bracket': + case 'forced_bracket': + $pre = '['; + $post = ']'; + break; + } + } + + $prefix_value = ''; + + if ($delim !== ' ') { + $prefix_value = ' '; + } + + $filtered = []; + + $same_string_quote = null; + foreach ($items as $item) { + if (\is_null($same_string_quote)) { + $same_string_quote = false; + if ($item[0] === Type::T_STRING) { + $same_string_quote = $item[1]; + foreach ($items as $ii) { + if ($ii[0] !== Type::T_STRING) { + $same_string_quote = false; + break; + } + } + } + } + if ($item[0] === Type::T_NULL) { + continue; + } + if ($same_string_quote === '"' && $item[0] === Type::T_STRING && $item[1]) { + $item[1] = $same_string_quote; + } + + $compiled = $this->compileValue($item, $quote); + + if ($prefix_value && \strlen($compiled)) { + $compiled = $prefix_value . $compiled; + } + + $filtered[] = $compiled; + } + + return $pre . substr(implode("$delim", $filtered), \strlen($prefix_value)) . $post; + + case Type::T_MAP: + $keys = $value[1]; + $values = $value[2]; + $filtered = []; + + for ($i = 0, $s = \count($keys); $i < $s; $i++) { + $filtered[$this->compileValue($keys[$i], $quote)] = $this->compileValue($values[$i], $quote); + } + + array_walk($filtered, function (&$value, $key) { + $value = $key . ': ' . $value; + }); + + return '(' . implode(', ', $filtered) . ')'; + + case Type::T_INTERPOLATED: + // node created by extractInterpolation + list(, $interpolate, $left, $right) = $value; + list(,, $whiteLeft, $whiteRight) = $interpolate; + + $delim = $left[1]; + + if ($delim && $delim !== ' ' && ! $whiteLeft) { + $delim .= ' '; + } + + $left = \count($left[2]) > 0 + ? $this->compileValue($left, $quote) . $delim . $whiteLeft + : ''; + + $delim = $right[1]; + + if ($delim && $delim !== ' ') { + $delim .= ' '; + } + + $right = \count($right[2]) > 0 ? + $whiteRight . $delim . $this->compileValue($right, $quote) : ''; + + return $left . $this->compileValue($interpolate, $quote) . $right; + + case Type::T_INTERPOLATE: + // strip quotes if it's a string + $reduced = $this->reduce($value[1]); + + if ($reduced instanceof Number) { + return $this->compileValue($reduced, $quote); + } + + switch ($reduced[0]) { + case Type::T_LIST: + $reduced = $this->extractInterpolation($reduced); + + if ($reduced[0] !== Type::T_LIST) { + break; + } + + list(, $delim, $items) = $reduced; + + if ($delim !== ' ') { + $delim .= ' '; + } + + $filtered = []; + + foreach ($items as $item) { + if ($item[0] === Type::T_NULL) { + continue; + } + + if ($item[0] === Type::T_STRING) { + $filtered[] = $this->compileStringContent($item, $quote); + } elseif ($item[0] === Type::T_KEYWORD) { + $filtered[] = $item[1]; + } else { + $filtered[] = $this->compileValue($item, $quote); + } + } + + $reduced = [Type::T_KEYWORD, implode("$delim", $filtered)]; + break; + + case Type::T_STRING: + $reduced = [Type::T_STRING, '', [$this->compileStringContent($reduced)]]; + break; + + case Type::T_NULL: + $reduced = [Type::T_KEYWORD, '']; + } + + return $this->compileValue($reduced, $quote); + + case Type::T_NULL: + return 'null'; + + case Type::T_COMMENT: + return $this->compileCommentValue($value); + + default: + throw $this->error('unknown value type: ' . json_encode($value)); + } + } + + /** + * @param array|Number $value + * + * @return string + */ + protected function compileDebugValue($value) + { + $value = $this->reduce($value, true); + + if ($value instanceof Number) { + return $this->compileValue($value); + } + + switch ($value[0]) { + case Type::T_STRING: + return $this->compileStringContent($value); + + default: + return $this->compileValue($value); + } + } + + /** + * Flatten list + * + * @param array $list + * + * @return string + * + * @deprecated + */ + protected function flattenList($list) + { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + + return $this->compileValue($list); + } + + /** + * Gets the text of a Sass string + * + * Calling this method on anything else than a SassString is unsupported. Use {@see assertString} first + * to ensure that the value is indeed a string. + * + * @param array $value + * + * @return string + */ + public function getStringText(array $value) + { + if ($value[0] !== Type::T_STRING) { + throw new \InvalidArgumentException('The argument is not a sass string. Did you forgot to use "assertString"?'); + } + + return $this->compileStringContent($value); + } + + /** + * Compile string content + * + * @param array $string + * @param bool $quote + * + * @return string + */ + protected function compileStringContent($string, $quote = true) + { + $parts = []; + + foreach ($string[2] as $part) { + if (\is_array($part) || $part instanceof Number) { + $parts[] = $this->compileValue($part, $quote); + } else { + $parts[] = $part; + } + } + + return implode($parts); + } + + /** + * Extract interpolation; it doesn't need to be recursive, compileValue will handle that + * + * @param array $list + * + * @return array + */ + protected function extractInterpolation($list) + { + $items = $list[2]; + + foreach ($items as $i => $item) { + if ($item[0] === Type::T_INTERPOLATE) { + $before = [Type::T_LIST, $list[1], \array_slice($items, 0, $i)]; + $after = [Type::T_LIST, $list[1], \array_slice($items, $i + 1)]; + + return [Type::T_INTERPOLATED, $item, $before, $after]; + } + } + + return $list; + } + + /** + * Find the final set of selectors + * + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param \ScssPhp\ScssPhp\Block $selfParent + * + * @return array + */ + protected function multiplySelectors(Environment $env, $selfParent = null) + { + $envs = $this->compactEnv($env); + $selectors = []; + $parentSelectors = [[]]; + + $selfParentSelectors = null; + + if (! \is_null($selfParent) && $selfParent->selectors) { + $selfParentSelectors = $this->evalSelectors($selfParent->selectors); + } + + while ($env = array_pop($envs)) { + if (empty($env->selectors)) { + continue; + } + + $selectors = $env->selectors; + + do { + $stillHasSelf = false; + $prevSelectors = $selectors; + $selectors = []; + + foreach ($parentSelectors as $parent) { + foreach ($prevSelectors as $selector) { + if ($selfParentSelectors) { + foreach ($selfParentSelectors as $selfParent) { + // if no '&' in the selector, each call will give same result, only add once + $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent); + $selectors[serialize($s)] = $s; + } + } else { + $s = $this->joinSelectors($parent, $selector, $stillHasSelf); + $selectors[serialize($s)] = $s; + } + } + } + } while ($stillHasSelf); + + $parentSelectors = $selectors; + } + + $selectors = array_values($selectors); + + // case we are just starting a at-root : nothing to multiply but parentSelectors + if (! $selectors && $selfParentSelectors) { + $selectors = $selfParentSelectors; + } + + return $selectors; + } + + /** + * Join selectors; looks for & to replace, or append parent before child + * + * @param array $parent + * @param array $child + * @param boolean $stillHasSelf + * @param array $selfParentSelectors + + * @return array + */ + protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null) + { + $setSelf = false; + $out = []; + + foreach ($child as $part) { + $newPart = []; + + foreach ($part as $p) { + // only replace & once and should be recalled to be able to make combinations + if ($p === static::$selfSelector && $setSelf) { + $stillHasSelf = true; + } + + if ($p === static::$selfSelector && ! $setSelf) { + $setSelf = true; + + if (\is_null($selfParentSelectors)) { + $selfParentSelectors = $parent; + } + + foreach ($selfParentSelectors as $i => $parentPart) { + if ($i > 0) { + $out[] = $newPart; + $newPart = []; + } + + foreach ($parentPart as $pp) { + if (\is_array($pp)) { + $flatten = []; + + array_walk_recursive($pp, function ($a) use (&$flatten) { + $flatten[] = $a; + }); + + $pp = implode($flatten); + } + + $newPart[] = $pp; + } + } + } else { + $newPart[] = $p; + } + } + + $out[] = $newPart; + } + + return $setSelf ? $out : array_merge($parent, $child); + } + + /** + * Multiply media + * + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param array $childQueries + * + * @return array + */ + protected function multiplyMedia(Environment $env = null, $childQueries = null) + { + if ( + ! isset($env) || + ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA + ) { + return $childQueries; + } + + // plain old block, skip + if (empty($env->block->type)) { + return $this->multiplyMedia($env->parent, $childQueries); + } + + $parentQueries = isset($env->block->queryList) + ? $env->block->queryList + : [[[Type::T_MEDIA_VALUE, $env->block->value]]]; + + $store = [$this->env, $this->storeEnv]; + + $this->env = $env; + $this->storeEnv = null; + $parentQueries = $this->evaluateMediaQuery($parentQueries); + + list($this->env, $this->storeEnv) = $store; + + if (\is_null($childQueries)) { + $childQueries = $parentQueries; + } else { + $originalQueries = $childQueries; + $childQueries = []; + + foreach ($parentQueries as $parentQuery) { + foreach ($originalQueries as $childQuery) { + $childQueries[] = array_merge( + $parentQuery, + [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]], + $childQuery + ); + } + } + } + + return $this->multiplyMedia($env->parent, $childQueries); + } + + /** + * Convert env linked list to stack + * + * @param Environment $env + * + * @return Environment[] + * + * @phpstan-return non-empty-array + */ + protected function compactEnv(Environment $env) + { + for ($envs = []; $env; $env = $env->parent) { + $envs[] = $env; + } + + return $envs; + } + + /** + * Convert env stack to singly linked list + * + * @param Environment[] $envs + * + * @return Environment + * + * @phpstan-param non-empty-array $envs + */ + protected function extractEnv($envs) + { + for ($env = null; $e = array_pop($envs);) { + $e->parent = $env; + $env = $e; + } + + return $env; + } + + /** + * Push environment + * + * @param \ScssPhp\ScssPhp\Block $block + * + * @return \ScssPhp\ScssPhp\Compiler\Environment + */ + protected function pushEnv(Block $block = null) + { + $env = new Environment(); + $env->parent = $this->env; + $env->parentStore = $this->storeEnv; + $env->store = []; + $env->block = $block; + $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0; + + $this->env = $env; + $this->storeEnv = null; + + return $env; + } + + /** + * Pop environment + * + * @return void + */ + protected function popEnv() + { + $this->storeEnv = $this->env->parentStore; + $this->env = $this->env->parent; + } + + /** + * Propagate vars from a just poped Env (used in @each and @for) + * + * @param array $store + * @param null|string[] $excludedVars + * + * @return void + */ + protected function backPropagateEnv($store, $excludedVars = null) + { + foreach ($store as $key => $value) { + if (empty($excludedVars) || ! \in_array($key, $excludedVars)) { + $this->set($key, $value, true); + } + } + } + + /** + * Get store environment + * + * @return \ScssPhp\ScssPhp\Compiler\Environment + */ + protected function getStoreEnv() + { + return isset($this->storeEnv) ? $this->storeEnv : $this->env; + } + + /** + * Set variable + * + * @param string $name + * @param mixed $value + * @param boolean $shadow + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param mixed $valueUnreduced + * + * @return void + */ + protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null) + { + $name = $this->normalizeName($name); + + if (! isset($env)) { + $env = $this->getStoreEnv(); + } + + if ($shadow) { + $this->setRaw($name, $value, $env, $valueUnreduced); + } else { + $this->setExisting($name, $value, $env, $valueUnreduced); + } + } + + /** + * Set existing variable + * + * @param string $name + * @param mixed $value + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param mixed $valueUnreduced + * + * @return void + */ + protected function setExisting($name, $value, Environment $env, $valueUnreduced = null) + { + $storeEnv = $env; + $specialContentKey = static::$namespaces['special'] . 'content'; + + $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%'; + + $maxDepth = 10000; + + for (;;) { + if ($maxDepth-- <= 0) { + break; + } + + if (\array_key_exists($name, $env->store)) { + break; + } + + if (! $hasNamespace && isset($env->marker)) { + if (! empty($env->store[$specialContentKey])) { + $env = $env->store[$specialContentKey]->scope; + continue; + } + + if (! empty($env->declarationScopeParent)) { + $env = $env->declarationScopeParent; + continue; + } else { + $env = $storeEnv; + break; + } + } + + if (isset($env->parentStore)) { + $env = $env->parentStore; + } elseif (isset($env->parent)) { + $env = $env->parent; + } else { + $env = $storeEnv; + break; + } + } + + $env->store[$name] = $value; + + if ($valueUnreduced) { + $env->storeUnreduced[$name] = $valueUnreduced; + } + } + + /** + * Set raw variable + * + * @param string $name + * @param mixed $value + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param mixed $valueUnreduced + * + * @return void + */ + protected function setRaw($name, $value, Environment $env, $valueUnreduced = null) + { + $env->store[$name] = $value; + + if ($valueUnreduced) { + $env->storeUnreduced[$name] = $valueUnreduced; + } + } + + /** + * Get variable + * + * @internal + * + * @param string $name + * @param boolean $shouldThrow + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param boolean $unreduced + * + * @return mixed|null + */ + public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false) + { + $normalizedName = $this->normalizeName($name); + $specialContentKey = static::$namespaces['special'] . 'content'; + + if (! isset($env)) { + $env = $this->getStoreEnv(); + } + + $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%'; + + $maxDepth = 10000; + + for (;;) { + if ($maxDepth-- <= 0) { + break; + } + + if (\array_key_exists($normalizedName, $env->store)) { + if ($unreduced && isset($env->storeUnreduced[$normalizedName])) { + return $env->storeUnreduced[$normalizedName]; + } + + return $env->store[$normalizedName]; + } + + if (! $hasNamespace && isset($env->marker)) { + if (! empty($env->store[$specialContentKey])) { + $env = $env->store[$specialContentKey]->scope; + continue; + } + + if (! empty($env->declarationScopeParent)) { + $env = $env->declarationScopeParent; + } else { + $env = $this->rootEnv; + } + continue; + } + + if (isset($env->parentStore)) { + $env = $env->parentStore; + } elseif (isset($env->parent)) { + $env = $env->parent; + } else { + break; + } + } + + if ($shouldThrow) { + throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : '')); + } + + // found nothing + return null; + } + + /** + * Has variable? + * + * @param string $name + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * + * @return boolean + */ + protected function has($name, Environment $env = null) + { + return ! \is_null($this->get($name, false, $env)); + } + + /** + * Inject variables + * + * @param array $args + * + * @return void + */ + protected function injectVariables(array $args) + { + if (empty($args)) { + return; + } + + $parser = $this->parserFactory(__METHOD__); + + foreach ($args as $name => $strValue) { + if ($name[0] === '$') { + $name = substr($name, 1); + } + + if (!\is_string($strValue) || ! $parser->parseValue($strValue, $value)) { + $value = $this->coerceValue($strValue); + } + + $this->set($name, $value); + } + } + + /** + * Replaces variables. + * + * @param array $variables + * + * @return void + */ + public function replaceVariables(array $variables) + { + $this->registeredVars = []; + $this->addVariables($variables); + } + + /** + * Replaces variables. + * + * @param array $variables + * + * @return void + */ + public function addVariables(array $variables) + { + $triggerWarning = false; + + foreach ($variables as $name => $value) { + if (!$value instanceof Number && !\is_array($value)) { + $triggerWarning = true; + } + + $this->registeredVars[$name] = $value; + } + + if ($triggerWarning) { + @trigger_error('Passing raw values to as custom variables to the Compiler is deprecated. Use "\ScssPhp\ScssPhp\ValueConverter::parseValue" or "\ScssPhp\ScssPhp\ValueConverter::fromPhp" to convert them instead.', E_USER_DEPRECATED); + } + } + + /** + * Set variables + * + * @api + * + * @param array $variables + * + * @return void + * + * @deprecated Use "addVariables" or "replaceVariables" instead. + */ + public function setVariables(array $variables) + { + @trigger_error('The method "setVariables" of the Compiler is deprecated. Use the "addVariables" method for the equivalent behavior or "replaceVariables" if merging with previous variables was not desired.'); + + $this->addVariables($variables); + } + + /** + * Unset variable + * + * @api + * + * @param string $name + * + * @return void + */ + public function unsetVariable($name) + { + unset($this->registeredVars[$name]); + } + + /** + * Returns list of variables + * + * @api + * + * @return array + */ + public function getVariables() + { + return $this->registeredVars; + } + + /** + * Adds to list of parsed files + * + * @internal + * + * @param string|null $path + * + * @return void + */ + public function addParsedFile($path) + { + if (! \is_null($path) && is_file($path)) { + $this->parsedFiles[realpath($path)] = filemtime($path); + } + } + + /** + * Returns list of parsed files + * + * @deprecated + * @return array + */ + public function getParsedFiles() + { + @trigger_error('The method "getParsedFiles" of the Compiler is deprecated. Use the "getIncludedFiles" method on the CompilationResult instance returned by compileString() instead. Be careful that the signature of the method is different.', E_USER_DEPRECATED); + return $this->parsedFiles; + } + + /** + * Add import path + * + * @api + * + * @param string|callable $path + * + * @return void + */ + public function addImportPath($path) + { + if (! \in_array($path, $this->importPaths)) { + $this->importPaths[] = $path; + } + } + + /** + * Set import paths + * + * @api + * + * @param string|array $path + * + * @return void + */ + public function setImportPaths($path) + { + $paths = (array) $path; + $actualImportPaths = array_filter($paths, function ($path) { + return $path !== ''; + }); + + $this->legacyCwdImportPath = \count($actualImportPaths) !== \count($paths); + + if ($this->legacyCwdImportPath) { + @trigger_error('Passing an empty string in the import paths to refer to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be used directly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED); + } + + $this->importPaths = $actualImportPaths; + } + + /** + * Set number precision + * + * @api + * + * @param integer $numberPrecision + * + * @return void + * + * @deprecated The number precision is not configurable anymore. The default is enough for all browsers. + */ + public function setNumberPrecision($numberPrecision) + { + @trigger_error('The number precision is not configurable anymore. ' + . 'The default is enough for all browsers.', E_USER_DEPRECATED); + } + + /** + * Sets the output style. + * + * @api + * + * @param string $style One of the OutputStyle constants + * + * @return void + * + * @phpstan-param OutputStyle::* $style + */ + public function setOutputStyle($style) + { + switch ($style) { + case OutputStyle::EXPANDED: + $this->formatter = Expanded::class; + break; + + case OutputStyle::COMPRESSED: + $this->formatter = Compressed::class; + break; + + default: + throw new \InvalidArgumentException(sprintf('Invalid output style "%s".', $style)); + } + } + + /** + * Set formatter + * + * @api + * + * @param string $formatterName + * + * @return void + * + * @deprecated Use {@see setOutputStyle} instead. + */ + public function setFormatter($formatterName) + { + if (!\in_array($formatterName, [Expanded::class, Compressed::class], true)) { + @trigger_error('Formatters other than Expanded and Compressed are deprecated.', E_USER_DEPRECATED); + } + @trigger_error('The method "setFormatter" is deprecated. Use "setOutputStyle" instead.', E_USER_DEPRECATED); + + $this->formatter = $formatterName; + } + + /** + * Set line number style + * + * @api + * + * @param string $lineNumberStyle + * + * @return void + * + * @deprecated The line number output is not supported anymore. Use source maps instead. + */ + public function setLineNumberStyle($lineNumberStyle) + { + @trigger_error('The line number output is not supported anymore. ' + . 'Use source maps instead.', E_USER_DEPRECATED); + } + + /** + * Configures the handling of non-ASCII outputs. + * + * If $charset is `true`, this will include a `@charset` declaration or a + * UTF-8 [byte-order mark][] if the stylesheet contains any non-ASCII + * characters. Otherwise, it will never include a `@charset` declaration or a + * byte-order mark. + * + * [byte-order mark]: https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8 + * + * @param bool $charset + * + * @return void + */ + public function setCharset($charset) + { + $this->charset = $charset; + } + + /** + * Enable/disable source maps + * + * @api + * + * @param integer $sourceMap + * + * @return void + * + * @phpstan-param self::SOURCE_MAP_* $sourceMap + */ + public function setSourceMap($sourceMap) + { + $this->sourceMap = $sourceMap; + } + + /** + * Set source map options + * + * @api + * + * @param array $sourceMapOptions + * + * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $sourceMapOptions + * + * @return void + */ + public function setSourceMapOptions($sourceMapOptions) + { + $this->sourceMapOptions = $sourceMapOptions; + } + + /** + * Register function + * + * @api + * + * @param string $name + * @param callable $callback + * @param string[]|null $argumentDeclaration + * + * @return void + */ + public function registerFunction($name, $callback, $argumentDeclaration = null) + { + if (self::isNativeFunction($name)) { + @trigger_error(sprintf('The "%s" function is a core sass function. Overriding it with a custom implementation through "%s" is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', $name, __METHOD__), E_USER_DEPRECATED); + } + + if ($argumentDeclaration === null) { + @trigger_error('Omitting the argument declaration when registering custom function is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', E_USER_DEPRECATED); + } + + $this->userFunctions[$this->normalizeName($name)] = [$callback, $argumentDeclaration]; + } + + /** + * Unregister function + * + * @api + * + * @param string $name + * + * @return void + */ + public function unregisterFunction($name) + { + unset($this->userFunctions[$this->normalizeName($name)]); + } + + /** + * Add feature + * + * @api + * + * @param string $name + * + * @return void + * + * @deprecated Registering additional features is deprecated. + */ + public function addFeature($name) + { + @trigger_error('Registering additional features is deprecated.', E_USER_DEPRECATED); + + $this->registeredFeatures[$name] = true; + } + + /** + * Import file + * + * @param string $path + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * + * @return void + */ + protected function importFile($path, OutputBlock $out) + { + $this->pushCallStack('import ' . $this->getPrettyPath($path)); + // see if tree is cached + $realPath = realpath($path); + + if (substr($path, -5) === '.sass') { + $this->sourceIndex = \count($this->sourceNames); + $this->sourceNames[] = $path; + $this->sourceLine = 1; + $this->sourceColumn = 1; + + throw $this->error('The Sass indented syntax is not implemented.'); + } + + if (isset($this->importCache[$realPath])) { + $this->handleImportLoop($realPath); + + $tree = $this->importCache[$realPath]; + } else { + $code = file_get_contents($path); + $parser = $this->parserFactory($path); + $tree = $parser->parse($code); + + $this->importCache[$realPath] = $tree; + } + + $currentDirectory = $this->currentDirectory; + $this->currentDirectory = dirname($path); + + $this->compileChildrenNoReturn($tree->children, $out); + $this->currentDirectory = $currentDirectory; + $this->popCallStack(); + } + + /** + * Save the imported files with their resolving path context + * + * @param string|null $currentDirectory + * @param string $path + * @param string $filePath + * + * @return void + */ + private function registerImport($currentDirectory, $path, $filePath) + { + $this->resolvedImports[] = ['currentDir' => $currentDirectory, 'path' => $path, 'filePath' => $filePath]; + } + + /** + * Detects whether the import is a CSS import. + * + * For legacy reasons, custom importers are called for those, allowing them + * to replace them with an actual Sass import. However this behavior is + * deprecated. Custom importers are expected to return null when they receive + * a CSS import. + * + * @param string $url + * + * @return bool + */ + public static function isCssImport($url) + { + return 1 === preg_match('~\.css$|^https?://|^//~', $url); + } + + /** + * Return the file path for an import url if it exists + * + * @internal + * + * @param string $url + * @param string|null $currentDir + * + * @return string|null + */ + public function findImport($url, $currentDir = null) + { + // Vanilla css and external requests. These are not meant to be Sass imports. + // Callback importers are still called for BC. + if (self::isCssImport($url)) { + foreach ($this->importPaths as $dir) { + if (\is_string($dir)) { + continue; + } + + if (\is_callable($dir)) { + // check custom callback for import path + $file = \call_user_func($dir, $url); + + if (! \is_null($file)) { + if (\is_array($dir)) { + $callableDescription = (\is_object($dir[0]) ? \get_class($dir[0]) : $dir[0]).'::'.$dir[1]; + } elseif ($dir instanceof \Closure) { + $r = new \ReflectionFunction($dir); + if (false !== strpos($r->name, '{closure}')) { + $callableDescription = sprintf('closure{%s:%s}', $r->getFileName(), $r->getStartLine()); + } elseif ($class = $r->getClosureScopeClass()) { + $callableDescription = $class->name.'::'.$r->name; + } else { + $callableDescription = $r->name; + } + } elseif (\is_object($dir)) { + $callableDescription = \get_class($dir) . '::__invoke'; + } else { + $callableDescription = 'callable'; // Fallback if we don't have a dedicated description + } + @trigger_error(sprintf('Returning a file to import for CSS or external references in custom importer callables is deprecated and will not be supported anymore in ScssPhp 2.0. This behavior is not compliant with the Sass specification. Update your "%s" importer.', $callableDescription), E_USER_DEPRECATED); + + return $file; + } + } + } + return null; + } + + if (!\is_null($currentDir)) { + $relativePath = $this->resolveImportPath($url, $currentDir); + + if (!\is_null($relativePath)) { + return $relativePath; + } + } + + foreach ($this->importPaths as $dir) { + if (\is_string($dir)) { + $path = $this->resolveImportPath($url, $dir); + + if (!\is_null($path)) { + return $path; + } + } elseif (\is_callable($dir)) { + // check custom callback for import path + $file = \call_user_func($dir, $url); + + if (! \is_null($file)) { + return $file; + } + } + } + + if ($this->legacyCwdImportPath) { + $path = $this->resolveImportPath($url, getcwd()); + + if (!\is_null($path)) { + @trigger_error('Resolving imports relatively to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be added as an import path explicitly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED); + + return $path; + } + } + + throw $this->error("`$url` file not found for @import"); + } + + /** + * @param string $url + * @param string $baseDir + * + * @return string|null + */ + private function resolveImportPath($url, $baseDir) + { + $path = Path::join($baseDir, $url); + + $hasExtension = preg_match('/.s[ac]ss$/', $url); + + if ($hasExtension) { + return $this->checkImportPathConflicts($this->tryImportPath($path)); + } + + $result = $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path)); + + if (!\is_null($result)) { + return $result; + } + + return $this->tryImportPathAsDirectory($path); + } + + /** + * @param string[] $paths + * + * @return string|null + */ + private function checkImportPathConflicts(array $paths) + { + if (\count($paths) === 0) { + return null; + } + + if (\count($paths) === 1) { + return $paths[0]; + } + + $formattedPrettyPaths = []; + + foreach ($paths as $path) { + $formattedPrettyPaths[] = ' ' . $this->getPrettyPath($path); + } + + throw $this->error("It's not clear which file to import. Found:\n" . implode("\n", $formattedPrettyPaths)); + } + + /** + * @param string $path + * + * @return string[] + */ + private function tryImportPathWithExtensions($path) + { + $result = array_merge( + $this->tryImportPath($path.'.sass'), + $this->tryImportPath($path.'.scss') + ); + + if ($result) { + return $result; + } + + return $this->tryImportPath($path.'.css'); + } + + /** + * @param string $path + * + * @return string[] + */ + private function tryImportPath($path) + { + $partial = dirname($path).'/_'.basename($path); + + $candidates = []; + + if (is_file($partial)) { + $candidates[] = $partial; + } + + if (is_file($path)) { + $candidates[] = $path; + } + + return $candidates; + } + + /** + * @param string $path + * + * @return string|null + */ + private function tryImportPathAsDirectory($path) + { + if (!is_dir($path)) { + return null; + } + + return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path.'/index')); + } + + /** + * @param string|null $path + * + * @return string + */ + private function getPrettyPath($path) + { + if ($path === null) { + return '(unknown file)'; + } + + $normalizedPath = $path; + $normalizedRootDirectory = $this->rootDirectory.'/'; + + if (\DIRECTORY_SEPARATOR === '\\') { + $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory); + $normalizedPath = str_replace('\\', '/', $path); + } + + if (0 === strpos($normalizedPath, $normalizedRootDirectory)) { + return substr($path, \strlen($normalizedRootDirectory)); + } + + return $path; + } + + /** + * Set encoding + * + * @api + * + * @param string|null $encoding + * + * @return void + * + * @deprecated Non-compliant support for other encodings than UTF-8 is deprecated. + */ + public function setEncoding($encoding) + { + if (!$encoding || strtolower($encoding) === 'utf-8') { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + } else { + @trigger_error(sprintf('The "%s" method is deprecated. Parsing will only support UTF-8 in ScssPhp 2.0. The non-UTF-8 parsing of ScssPhp 1.x is not spec compliant.', __METHOD__), E_USER_DEPRECATED); + } + + $this->encoding = $encoding; + } + + /** + * Ignore errors? + * + * @api + * + * @param boolean $ignoreErrors + * + * @return \ScssPhp\ScssPhp\Compiler + * + * @deprecated Ignoring Sass errors is not longer supported. + */ + public function setIgnoreErrors($ignoreErrors) + { + @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED); + + return $this; + } + + /** + * Get source position + * + * @api + * + * @return array + * + * @deprecated + */ + public function getSourcePosition() + { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + + $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : ''; + + return [$sourceFile, $this->sourceLine, $this->sourceColumn]; + } + + /** + * Throw error (exception) + * + * @api + * + * @param string $msg Message with optional sprintf()-style vararg parameters + * + * @throws \ScssPhp\ScssPhp\Exception\CompilerException + * + * @deprecated use "error" and throw the exception in the caller instead. + */ + public function throwError($msg) + { + @trigger_error( + 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead', + E_USER_DEPRECATED + ); + + throw $this->error(...func_get_args()); + } + + /** + * Build an error (exception) + * + * @internal + * + * @param string $msg Message with optional sprintf()-style vararg parameters + * + * @return CompilerException + */ + public function error($msg, ...$args) + { + if ($args) { + $msg = sprintf($msg, ...$args); + } + + if (! $this->ignoreCallStackMessage) { + $msg = $this->addLocationToMessage($msg); + } + + return new CompilerException($msg); + } + + /** + * @param string $msg + * + * @return string + */ + private function addLocationToMessage($msg) + { + $line = $this->sourceLine; + $column = $this->sourceColumn; + + $loc = isset($this->sourceNames[$this->sourceIndex]) + ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column" + : "line: $line, column: $column"; + + $msg = "$msg: $loc"; + + $callStackMsg = $this->callStackMessage(); + + if ($callStackMsg) { + $msg .= "\nCall Stack:\n" . $callStackMsg; + } + + return $msg; + } + + /** + * @param string $functionName + * @param array $ExpectedArgs + * @param int $nbActual + * @return CompilerException + * + * @deprecated + */ + public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual) + { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + + $nbExpected = \count($ExpectedArgs); + + if ($nbActual > $nbExpected) { + return $this->error( + 'Error: Only %d arguments allowed in %s(), but %d were passed.', + $nbExpected, + $functionName, + $nbActual + ); + } else { + $missing = []; + + while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) { + array_unshift($missing, array_pop($ExpectedArgs)); + } + + return $this->error( + 'Error: %s() argument%s %s missing.', + $functionName, + count($missing) > 1 ? 's' : '', + implode(', ', $missing) + ); + } + } + + /** + * Beautify call stack for output + * + * @param boolean $all + * @param int|null $limit + * + * @return string + */ + protected function callStackMessage($all = false, $limit = null) + { + $callStackMsg = []; + $ncall = 0; + + if ($this->callStack) { + foreach (array_reverse($this->callStack) as $call) { + if ($all || (isset($call['n']) && $call['n'])) { + $msg = '#' . $ncall++ . ' ' . $call['n'] . ' '; + $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]]) + ? $this->getPrettyPath($this->sourceNames[$call[Parser::SOURCE_INDEX]]) + : '(unknown file)'); + $msg .= ' on line ' . $call[Parser::SOURCE_LINE]; + + $callStackMsg[] = $msg; + + if (! \is_null($limit) && $ncall > $limit) { + break; + } + } + } + } + + return implode("\n", $callStackMsg); + } + + /** + * Handle import loop + * + * @param string $name + * + * @throws \Exception + */ + protected function handleImportLoop($name) + { + for ($env = $this->env; $env; $env = $env->parent) { + if (! $env->block) { + continue; + } + + $file = $this->sourceNames[$env->block->sourceIndex]; + + if ($file === null) { + continue; + } + + if (realpath($file) === $name) { + throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file)); + } + } + } + + /** + * Call SCSS @function + * + * @param Object $func + * @param array $argValues + * + * @return array|Number + */ + protected function callScssFunction($func, $argValues) + { + if (! $func) { + return static::$defaultValue; + } + $name = $func->name; + + $this->pushEnv(); + + // set the args + if (isset($func->args)) { + $this->applyArguments($func->args, $argValues); + } + + // throw away lines and children + $tmp = new OutputBlock(); + $tmp->lines = []; + $tmp->children = []; + + $this->env->marker = 'function'; + + if (! empty($func->parentEnv)) { + $this->env->declarationScopeParent = $func->parentEnv; + } else { + throw $this->error("@function $name() without parentEnv"); + } + + $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name); + + $this->popEnv(); + + return ! isset($ret) ? static::$defaultValue : $ret; + } + + /** + * Call built-in and registered (PHP) functions + * + * @param string $name + * @param callable $function + * @param array $prototype + * @param array $args + * + * @return array|Number|null + */ + protected function callNativeFunction($name, $function, $prototype, $args) + { + $libName = (is_array($function) ? end($function) : null); + $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args); + + if (\is_null($sorted_kwargs)) { + return null; + } + @list($sorted, $kwargs) = $sorted_kwargs; + + if ($name !== 'if') { + foreach ($sorted as &$val) { + if ($val !== null) { + $val = $this->reduce($val, true); + } + } + } + + $returnValue = \call_user_func($function, $sorted, $kwargs); + + if (! isset($returnValue)) { + return null; + } + + if (\is_array($returnValue) || $returnValue instanceof Number) { + return $returnValue; + } + + @trigger_error(sprintf('Returning a PHP value from the "%s" custom function is deprecated. A sass value must be returned instead.', $name), E_USER_DEPRECATED); + + return $this->coerceValue($returnValue); + } + + /** + * Get built-in function + * + * @param string $name Normalized name + * + * @return array + */ + protected function getBuiltinFunction($name) + { + $libName = self::normalizeNativeFunctionName($name); + return [$this, $libName]; + } + + /** + * Normalize native function name + * + * @internal + * + * @param string $name + * + * @return string + */ + public static function normalizeNativeFunctionName($name) + { + $name = str_replace("-", "_", $name); + $libName = 'lib' . preg_replace_callback( + '/_(.)/', + function ($m) { + return ucfirst($m[1]); + }, + ucfirst($name) + ); + return $libName; + } + + /** + * Check if a function is a native built-in scss function, for css parsing + * + * @internal + * + * @param string $name + * + * @return bool + */ + public static function isNativeFunction($name) + { + return method_exists(Compiler::class, self::normalizeNativeFunctionName($name)); + } + + /** + * Sorts keyword arguments + * + * @param string $functionName + * @param array|null $prototypes + * @param array $args + * + * @return array|null + */ + protected function sortNativeFunctionArgs($functionName, $prototypes, $args) + { + static $parser = null; + + if (! isset($prototypes)) { + $keyArgs = []; + $posArgs = []; + + if (\is_array($args) && \count($args) && \end($args) === static::$null) { + array_pop($args); + } + + // separate positional and keyword arguments + foreach ($args as $arg) { + list($key, $value) = $arg; + + if (empty($key) or empty($key[1])) { + $posArgs[] = empty($arg[2]) ? $value : $arg; + } else { + $keyArgs[$key[1]] = $value; + } + } + + return [$posArgs, $keyArgs]; + } + + // specific cases ? + if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) { + // notation 100 127 255 / 0 is in fact a simple list of 4 values + foreach ($args as $k => $arg) { + if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) { + $args[$k][1][2] = $this->extractSlashAlphaInColorFunction($arg[1][2]); + } + } + } + + list($positionalArgs, $namedArgs, $names, $separator, $hasSplat) = $this->evaluateArguments($args, false); + + if (! \is_array(reset($prototypes))) { + $prototypes = [$prototypes]; + } + + $parsedPrototypes = array_map([$this, 'parseFunctionPrototype'], $prototypes); + assert(!empty($parsedPrototypes)); + $matchedPrototype = $this->selectFunctionPrototype($parsedPrototypes, \count($positionalArgs), $names); + + $this->verifyPrototype($matchedPrototype, \count($positionalArgs), $names, $hasSplat); + + $vars = $this->applyArgumentsToDeclaration($matchedPrototype, $positionalArgs, $namedArgs, $separator); + + $finalArgs = []; + $keyArgs = []; + + foreach ($matchedPrototype['arguments'] as $argument) { + list($normalizedName, $originalName, $default) = $argument; + + if (isset($vars[$normalizedName])) { + $value = $vars[$normalizedName]; + } else { + $value = $default; + } + + // special null value as default: translate to real null here + if ($value === [Type::T_KEYWORD, 'null']) { + $value = null; + } + + $finalArgs[] = $value; + $keyArgs[$originalName] = $value; + } + + if ($matchedPrototype['rest_argument'] !== null) { + $value = $vars[$matchedPrototype['rest_argument']]; + + $finalArgs[] = $value; + $keyArgs[$matchedPrototype['rest_argument']] = $value; + } + + return [$finalArgs, $keyArgs]; + } + + /** + * Parses a function prototype to the internal representation of arguments. + * + * The input is an array of strings describing each argument, as supported + * in {@see registerFunction}. Argument names don't include the `$`. + * The output contains the list of positional argument, with their normalized + * name (underscores are replaced by dashes), their original name (to be used + * in case of error reporting) and their default value. The output also contains + * the normalized name of the rest argument, or null if the function prototype + * is not variadic. + * + * @param string[] $prototype + * + * @return array + * @phpstan-return array{arguments: list, rest_argument: string|null} + */ + private function parseFunctionPrototype(array $prototype) + { + static $parser = null; + + $arguments = []; + $restArgument = null; + + foreach ($prototype as $p) { + if (null !== $restArgument) { + throw new \InvalidArgumentException('The argument declaration is invalid. The rest argument must be the last one.'); + } + + $default = null; + $p = explode(':', $p, 2); + $name = str_replace('_', '-', $p[0]); + + if (isset($p[1])) { + $defaultSource = trim($p[1]); + + if ($defaultSource === 'null') { + // differentiate this null from the static::$null + $default = [Type::T_KEYWORD, 'null']; + } else { + if (\is_null($parser)) { + $parser = $this->parserFactory(__METHOD__); + } + + $parser->parseValue($defaultSource, $default); + } + } + + if (substr($name, -3) === '...') { + $restArgument = substr($name, 0, -3); + } else { + $arguments[] = [$name, $p[0], $default]; + } + } + + return [ + 'arguments' => $arguments, + 'rest_argument' => $restArgument, + ]; + } + + /** + * Returns the function prototype for the given positional and named arguments. + * + * If no exact match is found, finds the closest approximation. Note that this + * doesn't guarantee that $positional and $names are valid for the returned + * prototype. + * + * @param array[] $prototypes + * @param int $positional + * @param array $names A set of names, as both keys and values + * + * @return array + * + * @phpstan-param non-empty-list, rest_argument: string|null}> $prototypes + * @phpstan-return array{arguments: list, rest_argument: string|null} + */ + private function selectFunctionPrototype(array $prototypes, $positional, array $names) + { + $fuzzyMatch = null; + $minMismatchDistance = null; + + foreach ($prototypes as $prototype) { + // Ideally, find an exact match. + if ($this->checkPrototypeMatches($prototype, $positional, $names)) { + return $prototype; + } + + $mismatchDistance = \count($prototype['arguments']) - $positional; + + if ($minMismatchDistance !== null) { + if (abs($mismatchDistance) > abs($minMismatchDistance)) { + continue; + } + + // If two overloads have the same mismatch distance, favor the overload + // that has more arguments. + if (abs($mismatchDistance) === abs($minMismatchDistance) && $mismatchDistance < 0) { + continue; + } + } + + $minMismatchDistance = $mismatchDistance; + $fuzzyMatch = $prototype; + } + + return $fuzzyMatch; + } + + /** + * Checks whether the argument invocation matches the callable prototype. + * + * The rules are similar to {@see verifyPrototype}. The boolean return value + * avoids the overhead of building and catching exceptions when the reason of + * not matching the prototype does not need to be known. + * + * @param array $prototype + * @param int $positional + * @param array $names + * + * @return bool + * + * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype + */ + private function checkPrototypeMatches(array $prototype, $positional, array $names) + { + $nameUsed = 0; + + foreach ($prototype['arguments'] as $i => $argument) { + list ($name, $originalName, $default) = $argument; + + if ($i < $positional) { + if (isset($names[$name])) { + return false; + } + } elseif (isset($names[$name])) { + $nameUsed++; + } elseif ($default === null) { + return false; + } + } + + if ($prototype['rest_argument'] !== null) { + return true; + } + + if ($positional > \count($prototype['arguments'])) { + return false; + } + + if ($nameUsed < \count($names)) { + return false; + } + + return true; + } + + /** + * Verifies that the argument invocation is valid for the callable prototype. + * + * @param array $prototype + * @param int $positional + * @param array $names + * @param bool $hasSplat + * + * @return void + * + * @throws SassScriptException + * + * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype + */ + private function verifyPrototype(array $prototype, $positional, array $names, $hasSplat) + { + $nameUsed = 0; + + foreach ($prototype['arguments'] as $i => $argument) { + list ($name, $originalName, $default) = $argument; + + if ($i < $positional) { + if (isset($names[$name])) { + throw new SassScriptException(sprintf('Argument $%s was passed both by position and by name.', $originalName)); + } + } elseif (isset($names[$name])) { + $nameUsed++; + } elseif ($default === null) { + throw new SassScriptException(sprintf('Missing argument $%s', $originalName)); + } + } + + if ($prototype['rest_argument'] !== null) { + return; + } + + if ($positional > \count($prototype['arguments'])) { + $message = sprintf( + 'Only %d %sargument%s allowed, but %d %s passed.', + \count($prototype['arguments']), + empty($names) ? '' : 'positional ', + \count($prototype['arguments']) === 1 ? '' : 's', + $positional, + $positional === 1 ? 'was' : 'were' + ); + if (!$hasSplat) { + throw new SassScriptException($message); + } + + $message = $this->addLocationToMessage($message); + $message .= "\nThis will be an error in future versions of Sass."; + $this->logger->warn($message, true); + } + + if ($nameUsed < \count($names)) { + $unknownNames = array_values(array_diff($names, array_column($prototype['arguments'], 0))); + $lastName = array_pop($unknownNames); + $message = sprintf( + 'No argument%s named $%s%s.', + $unknownNames ? 's' : '', + $unknownNames ? implode(', $', $unknownNames) . ' or $' : '', + $lastName + ); + throw new SassScriptException($message); + } + } + + /** + * Evaluates the argument from the invocation. + * + * This returns several things about this invocation: + * - the list of positional arguments + * - the map of named arguments, indexed by normalized names + * - the set of names used in the arguments (that's an array using the normalized names as keys for O(1) access) + * - the separator used by the list using the splat operator, if any + * - a boolean indicator whether any splat argument (list or map) was used, to support the incomplete error reporting. + * + * @param array[] $args + * @param bool $reduce Whether arguments should be reduced to their value + * + * @return array + * + * @throws SassScriptException + * + * @phpstan-return array{0: list, 1: array, 2: array, 3: string|null, 4: bool} + */ + private function evaluateArguments(array $args, $reduce = true) + { + // this represents trailing commas + if (\count($args) && end($args) === static::$null) { + array_pop($args); + } + + $splatSeparator = null; + $keywordArgs = []; + $names = []; + $positionalArgs = []; + $hasKeywordArgument = false; + $hasSplat = false; + + foreach ($args as $arg) { + if (!empty($arg[0])) { + $hasKeywordArgument = true; + + assert(\is_string($arg[0][1])); + $name = str_replace('_', '-', $arg[0][1]); + + if (isset($keywordArgs[$name])) { + throw new SassScriptException(sprintf('Duplicate named argument $%s.', $arg[0][1])); + } + + $keywordArgs[$name] = $this->maybeReduce($reduce, $arg[1]); + $names[$name] = $name; + } elseif (! empty($arg[2])) { + // $arg[2] means a var followed by ... in the arg ($list... ) + $val = $this->reduce($arg[1], true); + $hasSplat = true; + + if ($val[0] === Type::T_LIST) { + foreach ($val[2] as $item) { + if (\is_null($splatSeparator)) { + $splatSeparator = $val[1]; + } + + $positionalArgs[] = $this->maybeReduce($reduce, $item); + } + + if (isset($val[3]) && \is_array($val[3])) { + foreach ($val[3] as $name => $item) { + assert(\is_string($name)); + + $normalizedName = str_replace('_', '-', $name); + + if (isset($keywordArgs[$normalizedName])) { + throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name)); + } + + $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item); + $names[$normalizedName] = $normalizedName; + $hasKeywordArgument = true; + } + } + } elseif ($val[0] === Type::T_MAP) { + foreach ($val[1] as $i => $name) { + $name = $this->compileStringContent($this->coerceString($name)); + $item = $val[2][$i]; + + if (! is_numeric($name)) { + $normalizedName = str_replace('_', '-', $name); + + if (isset($keywordArgs[$normalizedName])) { + throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name)); + } + + $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item); + $names[$normalizedName] = $normalizedName; + $hasKeywordArgument = true; + } else { + if (\is_null($splatSeparator)) { + $splatSeparator = $val[1]; + } + + $positionalArgs[] = $this->maybeReduce($reduce, $item); + } + } + } elseif ($val[0] !== Type::T_NULL) { // values other than null are treated a single-element lists, while null is the empty list + $positionalArgs[] = $this->maybeReduce($reduce, $val); + } + } elseif ($hasKeywordArgument) { + throw new SassScriptException('Positional arguments must come before keyword arguments.'); + } else { + $positionalArgs[] = $this->maybeReduce($reduce, $arg[1]); + } + } + + return [$positionalArgs, $keywordArgs, $names, $splatSeparator, $hasSplat]; + } + + /** + * @param bool $reduce + * @param array|Number $value + * + * @return array|Number + */ + private function maybeReduce($reduce, $value) + { + if ($reduce) { + return $this->reduce($value, true); + } + + return $value; + } + + /** + * Apply argument values per definition + * + * @param array[] $argDef + * @param array|null $argValues + * @param boolean $storeInEnv + * @param boolean $reduce + * only used if $storeInEnv = false + * + * @return array + * + * @phpstan-param list $argDef + * + * @throws \Exception + */ + protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true) + { + $output = []; + + if (\is_null($argValues)) { + $argValues = []; + } + + if ($storeInEnv) { + $storeEnv = $this->getStoreEnv(); + + $env = new Environment(); + $env->store = $storeEnv->store; + } + + $prototype = ['arguments' => [], 'rest_argument' => null]; + $originalRestArgumentName = null; + + foreach ($argDef as $i => $arg) { + list($name, $default, $isVariable) = $arg; + $normalizedName = str_replace('_', '-', $name); + + if ($isVariable) { + $originalRestArgumentName = $name; + $prototype['rest_argument'] = $normalizedName; + } else { + $prototype['arguments'][] = [$normalizedName, $name, !empty($default) ? $default : null]; + } + } + + list($positionalArgs, $namedArgs, $names, $splatSeparator, $hasSplat) = $this->evaluateArguments($argValues, $reduce); + + $this->verifyPrototype($prototype, \count($positionalArgs), $names, $hasSplat); + + $vars = $this->applyArgumentsToDeclaration($prototype, $positionalArgs, $namedArgs, $splatSeparator); + + foreach ($prototype['arguments'] as $argument) { + list($normalizedName, $name) = $argument; + + if (!isset($vars[$normalizedName])) { + continue; + } + + $val = $vars[$normalizedName]; + + if ($storeInEnv) { + $this->set($name, $this->reduce($val, true), true, $env); + } else { + $output[$name] = ($reduce ? $this->reduce($val, true) : $val); + } + } + + if ($prototype['rest_argument'] !== null) { + assert($originalRestArgumentName !== null); + $name = $originalRestArgumentName; + $val = $vars[$prototype['rest_argument']]; + + if ($storeInEnv) { + $this->set($name, $this->reduce($val, true), true, $env); + } else { + $output[$name] = ($reduce ? $this->reduce($val, true) : $val); + } + } + + if ($storeInEnv) { + $storeEnv->store = $env->store; + } + + foreach ($prototype['arguments'] as $argument) { + list($normalizedName, $name, $default) = $argument; + + if (isset($vars[$normalizedName])) { + continue; + } + assert($default !== null); + + if ($storeInEnv) { + $this->set($name, $this->reduce($default, true), true); + } else { + $output[$name] = ($reduce ? $this->reduce($default, true) : $default); + } + } + + return $output; + } + + /** + * Apply argument values per definition. + * + * This method assumes that the arguments are valid for the provided prototype. + * The validation with {@see verifyPrototype} must have been run before calling + * it. + * Arguments are returned as a map from the normalized argument names to the + * value. Additional arguments are collected in a sass argument list available + * under the name of the rest argument in the result. + * + * Defaults are not applied as they are resolved in a different environment. + * + * @param array $prototype + * @param array $positionalArgs + * @param array $namedArgs + * @param string|null $splatSeparator + * + * @return array + * + * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype + */ + private function applyArgumentsToDeclaration(array $prototype, array $positionalArgs, array $namedArgs, $splatSeparator) + { + $output = []; + $minLength = min(\count($positionalArgs), \count($prototype['arguments'])); + + for ($i = 0; $i < $minLength; $i++) { + list($name) = $prototype['arguments'][$i]; + $val = $positionalArgs[$i]; + + $output[$name] = $val; + } + + $restNamed = $namedArgs; + + for ($i = \count($positionalArgs); $i < \count($prototype['arguments']); $i++) { + $argument = $prototype['arguments'][$i]; + list($name) = $argument; + + if (isset($namedArgs[$name])) { + $val = $namedArgs[$name]; + unset($restNamed[$name]); + } else { + continue; + } + + $output[$name] = $val; + } + + if ($prototype['rest_argument'] !== null) { + $name = $prototype['rest_argument']; + $rest = array_values(array_slice($positionalArgs, \count($prototype['arguments']))); + + $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , $rest, $restNamed]; + + $output[$name] = $val; + } + + return $output; + } + + /** + * Coerce a php value into a scss one + * + * @param mixed $value + * + * @return array|Number + */ + protected function coerceValue($value) + { + if (\is_array($value) || $value instanceof Number) { + return $value; + } + + if (\is_bool($value)) { + return $this->toBool($value); + } + + if (\is_null($value)) { + return static::$null; + } + + if (is_numeric($value)) { + return new Number($value, ''); + } + + if ($value === '') { + return static::$emptyString; + } + + $value = [Type::T_KEYWORD, $value]; + $color = $this->coerceColor($value); + + if ($color) { + return $color; + } + + return $value; + } + + /** + * Coerce something to map + * + * @param array|Number $item + * + * @return array|Number + */ + protected function coerceMap($item) + { + if ($item[0] === Type::T_MAP) { + return $item; + } + + if ( + $item[0] === Type::T_LIST && + $item[2] === [] + ) { + return static::$emptyMap; + } + + return $item; + } + + /** + * Coerce something to list + * + * @param array|Number $item + * @param string $delim + * @param boolean $removeTrailingNull + * + * @return array + */ + protected function coerceList($item, $delim = ',', $removeTrailingNull = false) + { + if ($item instanceof Number) { + return [Type::T_LIST, $delim, [$item]]; + } + + if ($item[0] === Type::T_LIST) { + // remove trailing null from the list + if ($removeTrailingNull && end($item[2]) === static::$null) { + array_pop($item[2]); + } + + return $item; + } + + if ($item[0] === Type::T_MAP) { + $keys = $item[1]; + $values = $item[2]; + $list = []; + + for ($i = 0, $s = \count($keys); $i < $s; $i++) { + $key = $keys[$i]; + $value = $values[$i]; + + switch ($key[0]) { + case Type::T_LIST: + case Type::T_MAP: + case Type::T_STRING: + case Type::T_NULL: + break; + + default: + $key = [Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))]; + break; + } + + $list[] = [ + Type::T_LIST, + '', + [$key, $value] + ]; + } + + return [Type::T_LIST, ',', $list]; + } + + return [Type::T_LIST, $delim, [$item]]; + } + + /** + * Coerce color for expression + * + * @param array|Number $value + * + * @return array|Number + */ + protected function coerceForExpression($value) + { + if ($color = $this->coerceColor($value)) { + return $color; + } + + return $value; + } + + /** + * Coerce value to color + * + * @param array|Number $value + * @param bool $inRGBFunction + * + * @return array|null + */ + protected function coerceColor($value, $inRGBFunction = false) + { + if ($value instanceof Number) { + return null; + } + + switch ($value[0]) { + case Type::T_COLOR: + for ($i = 1; $i <= 3; $i++) { + if (! is_numeric($value[$i])) { + $cv = $this->compileRGBAValue($value[$i]); + + if (! is_numeric($cv)) { + return null; + } + + $value[$i] = $cv; + } + + if (isset($value[4])) { + if (! is_numeric($value[4])) { + $cv = $this->compileRGBAValue($value[4], true); + + if (! is_numeric($cv)) { + return null; + } + + $value[4] = $cv; + } + } + } + + return $value; + + case Type::T_LIST: + if ($inRGBFunction) { + if (\count($value[2]) == 3 || \count($value[2]) == 4) { + $color = $value[2]; + array_unshift($color, Type::T_COLOR); + + return $this->coerceColor($color); + } + } + + return null; + + case Type::T_KEYWORD: + if (! \is_string($value[1])) { + return null; + } + + $name = strtolower($value[1]); + + // hexa color? + if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) { + $nofValues = \strlen($m[1]); + + if (\in_array($nofValues, [3, 4, 6, 8])) { + $nbChannels = 3; + $color = []; + $num = hexdec($m[1]); + + switch ($nofValues) { + case 4: + $nbChannels = 4; + // then continuing with the case 3: + case 3: + for ($i = 0; $i < $nbChannels; $i++) { + $t = $num & 0xf; + array_unshift($color, $t << 4 | $t); + $num >>= 4; + } + + break; + + case 8: + $nbChannels = 4; + // then continuing with the case 6: + case 6: + for ($i = 0; $i < $nbChannels; $i++) { + array_unshift($color, $num & 0xff); + $num >>= 8; + } + + break; + } + + if ($nbChannels === 4) { + if ($color[3] === 255) { + $color[3] = 1; // fully opaque + } else { + $color[3] = round($color[3] / 255, Number::PRECISION); + } + } + + array_unshift($color, Type::T_COLOR); + + return $color; + } + } + + if ($rgba = Colors::colorNameToRGBa($name)) { + return isset($rgba[3]) + ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]] + : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]]; + } + + return null; + } + + return null; + } + + /** + * @param integer|Number $value + * @param boolean $isAlpha + * + * @return integer|mixed + */ + protected function compileRGBAValue($value, $isAlpha = false) + { + if ($isAlpha) { + return $this->compileColorPartValue($value, 0, 1, false); + } + + return $this->compileColorPartValue($value, 0, 255, true); + } + + /** + * @param mixed $value + * @param integer|float $min + * @param integer|float $max + * @param boolean $isInt + * + * @return integer|mixed + */ + protected function compileColorPartValue($value, $min, $max, $isInt = true) + { + if (! is_numeric($value)) { + if (\is_array($value)) { + $reduced = $this->reduce($value); + + if ($reduced instanceof Number) { + $value = $reduced; + } + } + + if ($value instanceof Number) { + if ($value->unitless()) { + $num = $value->getDimension(); + } elseif ($value->hasUnit('%')) { + $num = $max * $value->getDimension() / 100; + } else { + throw $this->error('Expected %s to have no units or "%%".', $value); + } + + $value = $num; + } elseif (\is_array($value)) { + $value = $this->compileValue($value); + } + } + + if (is_numeric($value)) { + if ($isInt) { + $value = round($value); + } + + $value = min($max, max($min, $value)); + + return $value; + } + + return $value; + } + + /** + * Coerce value to string + * + * @param array|Number $value + * + * @return array + */ + protected function coerceString($value) + { + if ($value[0] === Type::T_STRING) { + return $value; + } + + return [Type::T_STRING, '', [$this->compileValue($value)]]; + } + + /** + * Assert value is a string + * + * This method deals with internal implementation details of the value + * representation where unquoted strings can sometimes be stored under + * other types. + * The returned value is always using the T_STRING type. + * + * @api + * + * @param array|Number $value + * @param string|null $varName + * + * @return array + * + * @throws SassScriptException + */ + public function assertString($value, $varName = null) + { + // case of url(...) parsed a a function + if ($value[0] === Type::T_FUNCTION) { + $value = $this->coerceString($value); + } + + if (! \in_array($value[0], [Type::T_STRING, Type::T_KEYWORD])) { + $value = $this->compileValue($value); + throw SassScriptException::forArgument("$value is not a string.", $varName); + } + + return $this->coerceString($value); + } + + /** + * Coerce value to a percentage + * + * @param array|Number $value + * + * @return integer|float + * + * @deprecated + */ + protected function coercePercent($value) + { + @trigger_error(sprintf('"%s" is deprecated since 1.7.0.', __METHOD__), E_USER_DEPRECATED); + + if ($value instanceof Number) { + if ($value->hasUnit('%')) { + return $value->getDimension() / 100; + } + + return $value->getDimension(); + } + + return 0; + } + + /** + * Assert value is a map + * + * @api + * + * @param array|Number $value + * @param string|null $varName + * + * @return array + * + * @throws SassScriptException + */ + public function assertMap($value, $varName = null) + { + $value = $this->coerceMap($value); + + if ($value[0] !== Type::T_MAP) { + $value = $this->compileValue($value); + + throw SassScriptException::forArgument("$value is not a map.", $varName); + } + + return $value; + } + + /** + * Assert value is a list + * + * @api + * + * @param array|Number $value + * + * @return array + * + * @throws \Exception + */ + public function assertList($value) + { + if ($value[0] !== Type::T_LIST) { + throw $this->error('expecting list, %s received', $value[0]); + } + + return $value; + } + + /** + * Gets the keywords of an argument list. + * + * Keys in the returned array are normalized names (underscores are replaced with dashes) + * without the leading `$`. + * Calling this helper with anything that an argument list received for a rest argument + * of the function argument declaration is not supported. + * + * @param array|Number $value + * + * @return array + */ + public function getArgumentListKeywords($value) + { + if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) { + throw new \InvalidArgumentException('The argument is not a sass argument list.'); + } + + return $value[3]; + } + + /** + * Assert value is a color + * + * @api + * + * @param array|Number $value + * @param string|null $varName + * + * @return array + * + * @throws SassScriptException + */ + public function assertColor($value, $varName = null) + { + if ($color = $this->coerceColor($value)) { + return $color; + } + + $value = $this->compileValue($value); + + throw SassScriptException::forArgument("$value is not a color.", $varName); + } + + /** + * Assert value is a number + * + * @api + * + * @param array|Number $value + * @param string|null $varName + * + * @return Number + * + * @throws SassScriptException + */ + public function assertNumber($value, $varName = null) + { + if (!$value instanceof Number) { + $value = $this->compileValue($value); + throw SassScriptException::forArgument("$value is not a number.", $varName); + } + + return $value; + } + + /** + * Assert value is a integer + * + * @api + * + * @param array|Number $value + * @param string|null $varName + * + * @return integer + * + * @throws SassScriptException + */ + public function assertInteger($value, $varName = null) + { + $value = $this->assertNumber($value, $varName)->getDimension(); + if (round($value - \intval($value), Number::PRECISION) > 0) { + throw SassScriptException::forArgument("$value is not an integer.", $varName); + } + + return intval($value); + } + + /** + * Extract the ... / alpha on the last argument of channel arg + * in color functions + * + * @param array $args + * @return array + */ + private function extractSlashAlphaInColorFunction($args) + { + $last = end($args); + if (\count($args) === 3 && $last[0] === Type::T_EXPRESSION && $last[1] === '/') { + array_pop($args); + $args[] = $last[2]; + $args[] = $last[3]; + } + return $args; + } + + + /** + * Make sure a color's components don't go out of bounds + * + * @param array $c + * + * @return array + */ + protected function fixColor($c) + { + foreach ([1, 2, 3] as $i) { + if ($c[$i] < 0) { + $c[$i] = 0; + } + + if ($c[$i] > 255) { + $c[$i] = 255; + } + + if (!\is_int($c[$i])) { + $c[$i] = round($c[$i]); + } + } + + return $c; + } + + /** + * Convert RGB to HSL + * + * @internal + * + * @param integer $red + * @param integer $green + * @param integer $blue + * + * @return array + */ + public function toHSL($red, $green, $blue) + { + $min = min($red, $green, $blue); + $max = max($red, $green, $blue); + + $l = $min + $max; + $d = $max - $min; + + if ((int) $d === 0) { + $h = $s = 0; + } else { + if ($l < 255) { + $s = $d / $l; + } else { + $s = $d / (510 - $l); + } + + if ($red == $max) { + $h = 60 * ($green - $blue) / $d; + } elseif ($green == $max) { + $h = 60 * ($blue - $red) / $d + 120; + } elseif ($blue == $max) { + $h = 60 * ($red - $green) / $d + 240; + } + } + + return [Type::T_HSL, fmod($h + 360, 360), $s * 100, $l / 5.1]; + } + + /** + * Hue to RGB helper + * + * @param float $m1 + * @param float $m2 + * @param float $h + * + * @return float + */ + protected function hueToRGB($m1, $m2, $h) + { + if ($h < 0) { + $h += 1; + } elseif ($h > 1) { + $h -= 1; + } + + if ($h * 6 < 1) { + return $m1 + ($m2 - $m1) * $h * 6; + } + + if ($h * 2 < 1) { + return $m2; + } + + if ($h * 3 < 2) { + return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6; + } + + return $m1; + } + + /** + * Convert HSL to RGB + * + * @internal + * + * @param int|float $hue H from 0 to 360 + * @param int|float $saturation S from 0 to 100 + * @param int|float $lightness L from 0 to 100 + * + * @return array + */ + public function toRGB($hue, $saturation, $lightness) + { + if ($hue < 0) { + $hue += 360; + } + + $h = $hue / 360; + $s = min(100, max(0, $saturation)) / 100; + $l = min(100, max(0, $lightness)) / 100; + + $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s; + $m1 = $l * 2 - $m2; + + $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255; + $g = $this->hueToRGB($m1, $m2, $h) * 255; + $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255; + + $out = [Type::T_COLOR, $r, $g, $b]; + + return $out; + } + + /** + * Convert HWB to RGB + * https://www.w3.org/TR/css-color-4/#hwb-to-rgb + * + * @api + * + * @param integer $hue H from 0 to 360 + * @param integer $whiteness W from 0 to 100 + * @param integer $blackness B from 0 to 100 + * + * @return array + */ + private function HWBtoRGB($hue, $whiteness, $blackness) + { + $w = min(100, max(0, $whiteness)) / 100; + $b = min(100, max(0, $blackness)) / 100; + + $sum = $w + $b; + if ($sum > 1.0) { + $w = $w / $sum; + $b = $b / $sum; + } + $b = min(1.0 - $w, $b); + + $rgb = $this->toRGB($hue, 100, 50); + for($i = 1; $i < 4; $i++) { + $rgb[$i] *= (1.0 - $w - $b); + $rgb[$i] = round($rgb[$i] + 255 * $w + 0.0001); + } + + return $rgb; + } + + /** + * Convert RGB to HWB + * + * @api + * + * @param integer $red + * @param integer $green + * @param integer $blue + * + * @return array + */ + private function RGBtoHWB($red, $green, $blue) + { + $min = min($red, $green, $blue); + $max = max($red, $green, $blue); + + $d = $max - $min; + + if ((int) $d === 0) { + $h = 0; + } else { + + if ($red == $max) { + $h = 60 * ($green - $blue) / $d; + } elseif ($green == $max) { + $h = 60 * ($blue - $red) / $d + 120; + } elseif ($blue == $max) { + $h = 60 * ($red - $green) / $d + 240; + } + } + + return [Type::T_HWB, fmod($h, 360), $min / 255 * 100, 100 - $max / 255 *100]; + } + + + // Built in functions + + protected static $libCall = ['function', 'args...']; + protected function libCall($args) + { + $functionReference = $args[0]; + + if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) { + $name = $this->compileStringContent($this->coerceString($functionReference)); + $warning = "Passing a string to call() is deprecated and will be illegal\n" + . "in Sass 4.0. Use call(function-reference($name)) instead."; + Warn::deprecation($warning); + $functionReference = $this->libGetFunction([$this->assertString($functionReference, 'function')]); + } + + if ($functionReference === static::$null) { + return static::$null; + } + + if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) { + throw $this->error('Function reference expected, got ' . $functionReference[0]); + } + + $callArgs = [ + [null, $args[1], true] + ]; + + return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]); + } + + + protected static $libGetFunction = [ + ['name'], + ['name', 'css'] + ]; + protected function libGetFunction($args) + { + $name = $this->compileStringContent($this->assertString(array_shift($args), 'name')); + $isCss = false; + + if (count($args)) { + $isCss = array_shift($args); + $isCss = (($isCss === static::$true) ? true : false); + } + + if ($isCss) { + return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]]; + } + + return $this->getFunctionReference($name, true); + } + + protected static $libIf = ['condition', 'if-true', 'if-false:']; + protected function libIf($args) + { + list($cond, $t, $f) = $args; + + if (! $this->isTruthy($this->reduce($cond, true))) { + return $this->reduce($f, true); + } + + return $this->reduce($t, true); + } + + protected static $libIndex = ['list', 'value']; + protected function libIndex($args) + { + list($list, $value) = $args; + + if ( + $list[0] === Type::T_MAP || + $list[0] === Type::T_STRING || + $list[0] === Type::T_KEYWORD || + $list[0] === Type::T_INTERPOLATE + ) { + $list = $this->coerceList($list, ' '); + } + + if ($list[0] !== Type::T_LIST) { + return static::$null; + } + + // Numbers are represented with value objects, for which the PHP equality operator does not + // match the Sass rules (and we cannot overload it). As they are the only type of values + // represented with a value object for now, they require a special case. + if ($value instanceof Number) { + $key = 0; + foreach ($list[2] as $item) { + $key++; + $itemValue = $this->normalizeValue($item); + + if ($itemValue instanceof Number && $value->equals($itemValue)) { + return new Number($key, ''); + } + } + return static::$null; + } + + $values = []; + + + foreach ($list[2] as $item) { + $values[] = $this->normalizeValue($item); + } + + $key = array_search($this->normalizeValue($value), $values); + + return false === $key ? static::$null : new Number($key + 1, ''); + } + + protected static $libRgb = [ + ['color'], + ['color', 'alpha'], + ['channels'], + ['red', 'green', 'blue'], + ['red', 'green', 'blue', 'alpha'] ]; + protected function libRgb($args, $kwargs, $funcName = 'rgb') + { + switch (\count($args)) { + case 1: + if (! $color = $this->coerceColor($args[0], true)) { + $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; + } + break; + + case 3: + $color = [Type::T_COLOR, $args[0], $args[1], $args[2]]; + + if (! $color = $this->coerceColor($color)) { + $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; + } + + return $color; + + case 2: + if ($color = $this->coerceColor($args[0], true)) { + $alpha = $this->compileRGBAValue($args[1], true); + + if (is_numeric($alpha)) { + $color[4] = $alpha; + } else { + $color = [Type::T_STRING, '', + [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']]; + } + } else { + $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ')']]; + } + break; + + case 4: + default: + $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]]; + + if (! $color = $this->coerceColor($color)) { + $color = [Type::T_STRING, '', + [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']]; + } + break; + } + + return $color; + } + + protected static $libRgba = [ + ['color'], + ['color', 'alpha'], + ['channels'], + ['red', 'green', 'blue'], + ['red', 'green', 'blue', 'alpha'] ]; + protected function libRgba($args, $kwargs) + { + return $this->libRgb($args, $kwargs, 'rgba'); + } + + /** + * Helper function for adjust_color, change_color, and scale_color + * + * @param array $args + * @param string $operation + * @param callable $fn + * + * @return array + * + * @phpstan-param callable(float|int, float|int|null, float|int): (float|int) $fn + */ + protected function alterColor(array $args, $operation, $fn) + { + $color = $this->assertColor($args[0], 'color'); + + if ($args[1][2]) { + throw new SassScriptException('Only one positional argument is allowed. All other arguments must be passed by name.'); + } + + $kwargs = $this->getArgumentListKeywords($args[1]); + + $scale = $operation === 'scale'; + $change = $operation === 'change'; + + /** + * @param string $name + * @param float|int $max + * @param bool $checkPercent + * @param bool $assertPercent + * + * @return float|int|null + */ + $getParam = function ($name, $max, $checkPercent = false, $assertPercent = false) use (&$kwargs, $scale, $change) { + if (!isset($kwargs[$name])) { + return null; + } + + $number = $this->assertNumber($kwargs[$name], $name); + unset($kwargs[$name]); + + if (!$scale && $checkPercent) { + if (!$number->hasUnit('%')) { + $warning = $this->error("{$name} Passing a number `$number` without unit % is deprecated."); + $this->logger->warn($warning->getMessage(), true); + } + } + + if ($scale || $assertPercent) { + $number->assertUnit('%', $name); + } + + if ($scale) { + $max = 100; + } + + return $number->valueInRange($change ? 0 : -$max, $max, $name); + }; + + $alpha = $getParam('alpha', 1); + $red = $getParam('red', 255); + $green = $getParam('green', 255); + $blue = $getParam('blue', 255); + + if ($scale || !isset($kwargs['hue'])) { + $hue = null; + } else { + $hueNumber = $this->assertNumber($kwargs['hue'], 'hue'); + unset($kwargs['hue']); + $hue = $hueNumber->getDimension(); + } + $saturation = $getParam('saturation', 100, true); + $lightness = $getParam('lightness', 100, true); + $whiteness = $getParam('whiteness', 100, false, true); + $blackness = $getParam('blackness', 100, false, true); + + if (!empty($kwargs)) { + $unknownNames = array_keys($kwargs); + $lastName = array_pop($unknownNames); + $message = sprintf( + 'No argument%s named $%s%s.', + $unknownNames ? 's' : '', + $unknownNames ? implode(', $', $unknownNames) . ' or $' : '', + $lastName + ); + throw new SassScriptException($message); + } + + $hasRgb = $red !== null || $green !== null || $blue !== null; + $hasSL = $saturation !== null || $lightness !== null; + $hasWB = $whiteness !== null || $blackness !== null; + $found = false; + + if ($hasRgb && ($hasSL || $hasWB || $hue !== null)) { + throw new SassScriptException(sprintf('RGB parameters may not be passed along with %s parameters.', $hasWB ? 'HWB' : 'HSL')); + } + + if ($hasWB && $hasSL) { + throw new SassScriptException('HSL parameters may not be passed along with HWB parameters.'); + } + + if ($hasRgb) { + $color[1] = round($fn($color[1], $red, 255)); + $color[2] = round($fn($color[2], $green, 255)); + $color[3] = round($fn($color[3], $blue, 255)); + } elseif ($hasWB) { + $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]); + if ($hue !== null) { + $hwb[1] = $change ? $hue : $hwb[1] + $hue; + } + $hwb[2] = $fn($hwb[2], $whiteness, 100); + $hwb[3] = $fn($hwb[3], $blackness, 100); + + $rgb = $this->HWBtoRGB($hwb[1], $hwb[2], $hwb[3]); + + if (isset($color[4])) { + $rgb[4] = $color[4]; + } + + $color = $rgb; + } elseif ($hue !== null || $hasSL) { + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + if ($hue !== null) { + $hsl[1] = $change ? $hue : $hsl[1] + $hue; + } + $hsl[2] = $fn($hsl[2], $saturation, 100); + $hsl[3] = $fn($hsl[3], $lightness, 100); + + $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); + + if (isset($color[4])) { + $rgb[4] = $color[4]; + } + + $color = $rgb; + } + + if ($alpha !== null) { + $existingAlpha = isset($color[4]) ? $color[4] : 1; + $color[4] = $fn($existingAlpha, $alpha, 1); + } + + return $color; + } + + protected static $libAdjustColor = ['color', 'kwargs...']; + protected function libAdjustColor($args) + { + return $this->alterColor($args, 'adjust', function ($base, $alter, $max) { + if ($alter === null) { + return $base; + } + + $new = $base + $alter; + + if ($new < 0) { + return 0; + } + + if ($new > $max) { + return $max; + } + + return $new; + }); + } + + protected static $libChangeColor = ['color', 'kwargs...']; + protected function libChangeColor($args) + { + return $this->alterColor($args,'change', function ($base, $alter, $max) { + if ($alter === null) { + return $base; + } + + return $alter; + }); + } + + protected static $libScaleColor = ['color', 'kwargs...']; + protected function libScaleColor($args) + { + return $this->alterColor($args, 'scale', function ($base, $scale, $max) { + if ($scale === null) { + return $base; + } + + $scale = $scale / 100; + + if ($scale < 0) { + return $base * $scale + $base; + } + + return ($max - $base) * $scale + $base; + }); + } + + protected static $libIeHexStr = ['color']; + protected function libIeHexStr($args) + { + $color = $this->coerceColor($args[0]); + + if (\is_null($color)) { + throw $this->error('Error: argument `$color` of `ie-hex-str($color)` must be a color'); + } + + $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255; + + return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]]; + } + + protected static $libRed = ['color']; + protected function libRed($args) + { + $color = $this->coerceColor($args[0]); + + if (\is_null($color)) { + throw $this->error('Error: argument `$color` of `red($color)` must be a color'); + } + + return new Number((int) $color[1], ''); + } + + protected static $libGreen = ['color']; + protected function libGreen($args) + { + $color = $this->coerceColor($args[0]); + + if (\is_null($color)) { + throw $this->error('Error: argument `$color` of `green($color)` must be a color'); + } + + return new Number((int) $color[2], ''); + } + + protected static $libBlue = ['color']; + protected function libBlue($args) + { + $color = $this->coerceColor($args[0]); + + if (\is_null($color)) { + throw $this->error('Error: argument `$color` of `blue($color)` must be a color'); + } + + return new Number((int) $color[3], ''); + } + + protected static $libAlpha = ['color']; + protected function libAlpha($args) + { + if ($color = $this->coerceColor($args[0])) { + return new Number(isset($color[4]) ? $color[4] : 1, ''); + } + + // this might be the IE function, so return value unchanged + return null; + } + + protected static $libOpacity = ['color']; + protected function libOpacity($args) + { + $value = $args[0]; + + if ($value instanceof Number) { + return null; + } + + return $this->libAlpha($args); + } + + // mix two colors + protected static $libMix = [ + ['color1', 'color2', 'weight:50%'], + ['color-1', 'color-2', 'weight:50%'] + ]; + protected function libMix($args) + { + list($first, $second, $weight) = $args; + + $first = $this->assertColor($first, 'color1'); + $second = $this->assertColor($second, 'color2'); + $weightScale = $this->assertNumber($weight, 'weight')->valueInRange(0, 100, 'weight') / 100; + + $firstAlpha = isset($first[4]) ? $first[4] : 1; + $secondAlpha = isset($second[4]) ? $second[4] : 1; + + $normalizedWeight = $weightScale * 2 - 1; + $alphaDistance = $firstAlpha - $secondAlpha; + + $combinedWeight = $normalizedWeight * $alphaDistance == -1 ? $normalizedWeight : ($normalizedWeight + $alphaDistance) / (1 + $normalizedWeight * $alphaDistance); + $weight1 = ($combinedWeight + 1) / 2.0; + $weight2 = 1.0 - $weight1; + + $new = [Type::T_COLOR, + $weight1 * $first[1] + $weight2 * $second[1], + $weight1 * $first[2] + $weight2 * $second[2], + $weight1 * $first[3] + $weight2 * $second[3], + ]; + + if ($firstAlpha != 1.0 || $secondAlpha != 1.0) { + $new[] = $firstAlpha * $weightScale + $secondAlpha * (1 - $weightScale); + } + + return $this->fixColor($new); + } + + protected static $libHsl = [ + ['channels'], + ['hue', 'saturation'], + ['hue', 'saturation', 'lightness'], + ['hue', 'saturation', 'lightness', 'alpha'] ]; + protected function libHsl($args, $kwargs, $funcName = 'hsl') + { + $args_to_check = $args; + + if (\count($args) == 1) { + if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) { + return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; + } + + $args = $args[0][2]; + $args_to_check = $kwargs['channels'][2]; + } + + if (\count($args) === 2) { + // if var() is used as an argument, return as a css function + foreach ($args as $arg) { + if ($arg[0] === Type::T_FUNCTION && in_array($arg[1], ['var'])) { + return null; + } + } + + throw new SassScriptException('Missing argument $lightness.'); + } + + foreach ($kwargs as $k => $arg) { + if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) { + return null; + } + } + + foreach ($args_to_check as $k => $arg) { + if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) { + if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) { + return null; + } + + $args[$k] = $this->stringifyFncallArgs($arg); + } + + if ( + $k >= 2 && count($args) === 4 && + in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && + in_array($arg[1], ['calc','env']) + ) { + return null; + } + } + + $hue = $this->reduce($args[0]); + $saturation = $this->reduce($args[1]); + $lightness = $this->reduce($args[2]); + $alpha = null; + + if (\count($args) === 4) { + $alpha = $this->compileColorPartValue($args[3], 0, 100, false); + + if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number || ! is_numeric($alpha)) { + return [Type::T_STRING, '', + [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']]; + } + } else { + if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number) { + return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; + } + } + + $hueValue = fmod($hue->getDimension(), 360); + + while ($hueValue < 0) { + $hueValue += 360; + } + + $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100))); + + if (! \is_null($alpha)) { + $color[4] = $alpha; + } + + return $color; + } + + protected static $libHsla = [ + ['channels'], + ['hue', 'saturation'], + ['hue', 'saturation', 'lightness'], + ['hue', 'saturation', 'lightness', 'alpha']]; + protected function libHsla($args, $kwargs) + { + return $this->libHsl($args, $kwargs, 'hsla'); + } + + protected static $libHue = ['color']; + protected function libHue($args) + { + $color = $this->assertColor($args[0], 'color'); + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + return new Number($hsl[1], 'deg'); + } + + protected static $libSaturation = ['color']; + protected function libSaturation($args) + { + $color = $this->assertColor($args[0], 'color'); + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + return new Number($hsl[2], '%'); + } + + protected static $libLightness = ['color']; + protected function libLightness($args) + { + $color = $this->assertColor($args[0], 'color'); + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + return new Number($hsl[3], '%'); + } + + /* + * Todo : a integrer dans le futur module color + protected static $libHwb = [ + ['channels'], + ['hue', 'whiteness', 'blackness'], + ['hue', 'whiteness', 'blackness', 'alpha'] ]; + protected function libHwb($args, $kwargs, $funcName = 'hwb') + { + $args_to_check = $args; + + if (\count($args) == 1) { + if ($args[0][0] !== Type::T_LIST) { + throw $this->error("Missing elements \$whiteness and \$blackness"); + } + + if (\trim($args[0][1])) { + throw $this->error("\$channels must be a space-separated list."); + } + + if (! empty($args[0]['enclosing'])) { + throw $this->error("\$channels must be an unbracketed list."); + } + + $args = $args[0][2]; + if (\count($args) > 3) { + throw $this->error("hwb() : Only 3 elements are allowed but ". \count($args) . "were passed"); + } + + $args_to_check = $this->extractSlashAlphaInColorFunction($kwargs['channels'][2]); + if (\count($args_to_check) !== \count($kwargs['channels'][2])) { + $args = $args_to_check; + } + } + + if (\count($args_to_check) < 2) { + throw $this->error("Missing elements \$whiteness and \$blackness"); + } + if (\count($args_to_check) < 3) { + throw $this->error("Missing element \$blackness"); + } + if (\count($args_to_check) > 4) { + throw $this->error("hwb() : Only 4 elements are allowed but ". \count($args) . "were passed"); + } + + foreach ($kwargs as $k => $arg) { + if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { + return null; + } + } + + foreach ($args_to_check as $k => $arg) { + if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { + if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) { + return null; + } + + $args[$k] = $this->stringifyFncallArgs($arg); + } + + if ( + $k >= 2 && count($args) === 4 && + in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && + in_array($arg[1], ['calc','env']) + ) { + return null; + } + } + + $hue = $this->reduce($args[0]); + $whiteness = $this->reduce($args[1]); + $blackness = $this->reduce($args[2]); + $alpha = null; + + if (\count($args) === 4) { + $alpha = $this->compileColorPartValue($args[3], 0, 1, false); + + if (! \is_numeric($alpha)) { + $val = $this->compileValue($args[3]); + throw $this->error("\$alpha: $val is not a number"); + } + } + + $this->assertNumber($hue, 'hue'); + $this->assertUnit($whiteness, ['%'], 'whiteness'); + $this->assertUnit($blackness, ['%'], 'blackness'); + + $this->assertRange($whiteness, 0, 100, "0% and 100%", "whiteness"); + $this->assertRange($blackness, 0, 100, "0% and 100%", "blackness"); + + $w = $whiteness->getDimension(); + $b = $blackness->getDimension(); + + $hueValue = $hue->getDimension() % 360; + + while ($hueValue < 0) { + $hueValue += 360; + } + + $color = $this->HWBtoRGB($hueValue, $w, $b); + + if (! \is_null($alpha)) { + $color[4] = $alpha; + } + + return $color; + } + + protected static $libWhiteness = ['color']; + protected function libWhiteness($args, $kwargs, $funcName = 'whiteness') { + + $color = $this->assertColor($args[0]); + $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]); + + return new Number($hwb[2], '%'); + } + + protected static $libBlackness = ['color']; + protected function libBlackness($args, $kwargs, $funcName = 'blackness') { + + $color = $this->assertColor($args[0]); + $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]); + + return new Number($hwb[3], '%'); + } + */ + + protected function adjustHsl($color, $idx, $amount) + { + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + $hsl[$idx] += $amount; + + if ($idx !== 1) { + // Clamp the saturation and lightness + $hsl[$idx] = min(max(0, $hsl[$idx]), 100); + } + + $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); + + if (isset($color[4])) { + $out[4] = $color[4]; + } + + return $out; + } + + protected static $libAdjustHue = ['color', 'degrees']; + protected function libAdjustHue($args) + { + $color = $this->assertColor($args[0], 'color'); + $degrees = $this->assertNumber($args[1], 'degrees')->getDimension(); + + return $this->adjustHsl($color, 1, $degrees); + } + + protected static $libLighten = ['color', 'amount']; + protected function libLighten($args) + { + $color = $this->assertColor($args[0], 'color'); + $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); + + return $this->adjustHsl($color, 3, $amount); + } + + protected static $libDarken = ['color', 'amount']; + protected function libDarken($args) + { + $color = $this->assertColor($args[0], 'color'); + $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); + + return $this->adjustHsl($color, 3, -$amount); + } + + protected static $libSaturate = [['color', 'amount'], ['amount']]; + protected function libSaturate($args) + { + $value = $args[0]; + + if (count($args) === 1) { + $this->assertNumber($args[0], 'amount'); + + return null; + } + + $color = $this->assertColor($args[0], 'color'); + $amount = $this->assertNumber($args[1], 'amount'); + + return $this->adjustHsl($color, 2, $amount->valueInRange(0, 100, 'amount')); + } + + protected static $libDesaturate = ['color', 'amount']; + protected function libDesaturate($args) + { + $color = $this->assertColor($args[0], 'color'); + $amount = $this->assertNumber($args[1], 'amount'); + + return $this->adjustHsl($color, 2, -$amount->valueInRange(0, 100, 'amount')); + } + + protected static $libGrayscale = ['color']; + protected function libGrayscale($args) + { + $value = $args[0]; + + if ($value instanceof Number) { + return null; + } + + return $this->adjustHsl($this->assertColor($value, 'color'), 2, -100); + } + + protected static $libComplement = ['color']; + protected function libComplement($args) + { + return $this->adjustHsl($this->assertColor($args[0], 'color'), 1, 180); + } + + protected static $libInvert = ['color', 'weight:100%']; + protected function libInvert($args) + { + $value = $args[0]; + + $weight = $this->assertNumber($args[1], 'weight'); + + if ($value instanceof Number) { + if ($weight->getDimension() != 100 || !$weight->hasUnit('%')) { + throw new SassScriptException('Only one argument may be passed to the plain-CSS invert() function.'); + } + + return null; + } + + $color = $this->assertColor($value, 'color'); + $inverted = $color; + $inverted[1] = 255 - $inverted[1]; + $inverted[2] = 255 - $inverted[2]; + $inverted[3] = 255 - $inverted[3]; + + return $this->libMix([$inverted, $color, $weight]); + } + + // increases opacity by amount + protected static $libOpacify = ['color', 'amount']; + protected function libOpacify($args) + { + $color = $this->assertColor($args[0], 'color'); + $amount = $this->assertNumber($args[1], 'amount'); + + $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount->valueInRange(0, 1, 'amount'); + $color[4] = min(1, max(0, $color[4])); + + return $color; + } + + protected static $libFadeIn = ['color', 'amount']; + protected function libFadeIn($args) + { + return $this->libOpacify($args); + } + + // decreases opacity by amount + protected static $libTransparentize = ['color', 'amount']; + protected function libTransparentize($args) + { + $color = $this->assertColor($args[0], 'color'); + $amount = $this->assertNumber($args[1], 'amount'); + + $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount->valueInRange(0, 1, 'amount'); + $color[4] = min(1, max(0, $color[4])); + + return $color; + } + + protected static $libFadeOut = ['color', 'amount']; + protected function libFadeOut($args) + { + return $this->libTransparentize($args); + } + + protected static $libUnquote = ['string']; + protected function libUnquote($args) + { + try { + $str = $this->assertString($args[0], 'string'); + } catch (SassScriptException $e) { + $value = $this->compileValue($args[0]); + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + + $message = "Passing $value, a non-string value, to unquote() +will be an error in future versions of Sass.\n on line $line of $fname"; + + $this->logger->warn($message, true); + + return $args[0]; + } + + $str[1] = ''; + + return $str; + } + + protected static $libQuote = ['string']; + protected function libQuote($args) + { + $value = $this->assertString($args[0], 'string'); + + $value[1] = '"'; + + return $value; + } + + protected static $libPercentage = ['number']; + protected function libPercentage($args) + { + $num = $this->assertNumber($args[0], 'number'); + $num->assertNoUnits('number'); + + return new Number($num->getDimension() * 100, '%'); + } + + protected static $libRound = ['number']; + protected function libRound($args) + { + $num = $this->assertNumber($args[0], 'number'); + + return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); + } + + protected static $libFloor = ['number']; + protected function libFloor($args) + { + $num = $this->assertNumber($args[0], 'number'); + + return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); + } + + protected static $libCeil = ['number']; + protected function libCeil($args) + { + $num = $this->assertNumber($args[0], 'number'); + + return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); + } + + protected static $libAbs = ['number']; + protected function libAbs($args) + { + $num = $this->assertNumber($args[0], 'number'); + + return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); + } + + protected static $libMin = ['numbers...']; + protected function libMin($args) + { + /** + * @var Number|null + */ + $min = null; + + foreach ($args[0][2] as $arg) { + $number = $this->assertNumber($arg); + + if (\is_null($min) || $min->greaterThan($number)) { + $min = $number; + } + } + + if (!\is_null($min)) { + return $min; + } + + throw $this->error('At least one argument must be passed.'); + } + + protected static $libMax = ['numbers...']; + protected function libMax($args) + { + /** + * @var Number|null + */ + $max = null; + + foreach ($args[0][2] as $arg) { + $number = $this->assertNumber($arg); + + if (\is_null($max) || $max->lessThan($number)) { + $max = $number; + } + } + + if (!\is_null($max)) { + return $max; + } + + throw $this->error('At least one argument must be passed.'); + } + + protected static $libLength = ['list']; + protected function libLength($args) + { + $list = $this->coerceList($args[0], ',', true); + + return new Number(\count($list[2]), ''); + } + + protected static $libListSeparator = ['list']; + protected function libListSeparator($args) + { + if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) { + return [Type::T_KEYWORD, 'space']; + } + + $list = $this->coerceList($args[0]); + + if (\count($list[2]) <= 1 && empty($list['enclosing'])) { + return [Type::T_KEYWORD, 'space']; + } + + if ($list[1] === ',') { + return [Type::T_KEYWORD, 'comma']; + } + + return [Type::T_KEYWORD, 'space']; + } + + protected static $libNth = ['list', 'n']; + protected function libNth($args) + { + $list = $this->coerceList($args[0], ',', false); + $n = $this->assertNumber($args[1])->getDimension(); + + if ($n > 0) { + $n--; + } elseif ($n < 0) { + $n += \count($list[2]); + } + + return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue; + } + + protected static $libSetNth = ['list', 'n', 'value']; + protected function libSetNth($args) + { + $list = $this->coerceList($args[0]); + $n = $this->assertNumber($args[1])->getDimension(); + + if ($n > 0) { + $n--; + } elseif ($n < 0) { + $n += \count($list[2]); + } + + if (! isset($list[2][$n])) { + throw $this->error('Invalid argument for "n"'); + } + + $list[2][$n] = $args[2]; + + return $list; + } + + protected static $libMapGet = ['map', 'key']; + protected function libMapGet($args) + { + $map = $this->assertMap($args[0], 'map'); + $key = $args[1]; + + if (! \is_null($key)) { + $key = $this->compileStringContent($this->coerceString($key)); + + for ($i = \count($map[1]) - 1; $i >= 0; $i--) { + if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { + return $map[2][$i]; + } + } + } + + return static::$null; + } + + protected static $libMapKeys = ['map']; + protected function libMapKeys($args) + { + $map = $this->assertMap($args[0], 'map'); + $keys = $map[1]; + + return [Type::T_LIST, ',', $keys]; + } + + protected static $libMapValues = ['map']; + protected function libMapValues($args) + { + $map = $this->assertMap($args[0], 'map'); + $values = $map[2]; + + return [Type::T_LIST, ',', $values]; + } + + protected static $libMapRemove = [ + ['map'], + ['map', 'key', 'keys...'], + ]; + protected function libMapRemove($args) + { + $map = $this->assertMap($args[0], 'map'); + + if (\count($args) === 1) { + return $map; + } + + $keys = []; + $keys[] = $this->compileStringContent($this->coerceString($args[1])); + + foreach ($args[2][2] as $key) { + $keys[] = $this->compileStringContent($this->coerceString($key)); + } + + for ($i = \count($map[1]) - 1; $i >= 0; $i--) { + if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) { + array_splice($map[1], $i, 1); + array_splice($map[2], $i, 1); + } + } + + return $map; + } + + protected static $libMapHasKey = ['map', 'key']; + protected function libMapHasKey($args) + { + $map = $this->assertMap($args[0], 'map'); + + return $this->toBool($this->mapHasKey($map, $args[1])); + } + + /** + * @param array|Number $keyValue + * + * @return bool + */ + private function mapHasKey(array $map, $keyValue) + { + $key = $this->compileStringContent($this->coerceString($keyValue)); + + for ($i = \count($map[1]) - 1; $i >= 0; $i--) { + if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { + return true; + } + } + + return false; + } + + protected static $libMapMerge = [ + ['map1', 'map2'], + ['map-1', 'map-2'] + ]; + protected function libMapMerge($args) + { + $map1 = $this->assertMap($args[0], 'map1'); + $map2 = $this->assertMap($args[1], 'map2'); + + foreach ($map2[1] as $i2 => $key2) { + $key = $this->compileStringContent($this->coerceString($key2)); + + foreach ($map1[1] as $i1 => $key1) { + if ($key === $this->compileStringContent($this->coerceString($key1))) { + $map1[2][$i1] = $map2[2][$i2]; + continue 2; + } + } + + $map1[1][] = $map2[1][$i2]; + $map1[2][] = $map2[2][$i2]; + } + + return $map1; + } + + protected static $libKeywords = ['args']; + protected function libKeywords($args) + { + $value = $args[0]; + + if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) { + $compiledValue = $this->compileValue($value); + + throw SassScriptException::forArgument($compiledValue . ' is not an argument list.', 'args'); + } + + $keys = []; + $values = []; + + foreach ($this->getArgumentListKeywords($value) as $name => $arg) { + $keys[] = [Type::T_KEYWORD, $name]; + $values[] = $arg; + } + + return [Type::T_MAP, $keys, $values]; + } + + protected static $libIsBracketed = ['list']; + protected function libIsBracketed($args) + { + $list = $args[0]; + $this->coerceList($list, ' '); + + if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') { + return self::$true; + } + + return self::$false; + } + + /** + * @param array $list1 + * @param array|Number|null $sep + * + * @return string + * @throws CompilerException + */ + protected function listSeparatorForJoin($list1, $sep) + { + if (! isset($sep)) { + return $list1[1]; + } + + switch ($this->compileValue($sep)) { + case 'comma': + return ','; + + case 'space': + return ' '; + + default: + return $list1[1]; + } + } + + protected static $libJoin = ['list1', 'list2', 'separator:null', 'bracketed:auto']; + protected function libJoin($args) + { + list($list1, $list2, $sep, $bracketed) = $args; + + $list1 = $this->coerceList($list1, ' ', true); + $list2 = $this->coerceList($list2, ' ', true); + $sep = $this->listSeparatorForJoin($list1, $sep); + + if ($bracketed === static::$true) { + $bracketed = true; + } elseif ($bracketed === static::$false) { + $bracketed = false; + } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) { + $bracketed = 'auto'; + } elseif ($bracketed === static::$null) { + $bracketed = false; + } else { + $bracketed = $this->compileValue($bracketed); + $bracketed = ! ! $bracketed; + + if ($bracketed === true) { + $bracketed = true; + } + } + + if ($bracketed === 'auto') { + $bracketed = false; + + if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') { + $bracketed = true; + } + } + + $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])]; + + if (isset($list1['enclosing'])) { + $res['enlcosing'] = $list1['enclosing']; + } + + if ($bracketed) { + $res['enclosing'] = 'bracket'; + } + + return $res; + } + + protected static $libAppend = ['list', 'val', 'separator:null']; + protected function libAppend($args) + { + list($list1, $value, $sep) = $args; + + $list1 = $this->coerceList($list1, ' ', true); + $sep = $this->listSeparatorForJoin($list1, $sep); + $res = [Type::T_LIST, $sep, array_merge($list1[2], [$value])]; + + if (isset($list1['enclosing'])) { + $res['enclosing'] = $list1['enclosing']; + } + + return $res; + } + + protected static $libZip = ['lists...']; + protected function libZip($args) + { + $argLists = []; + foreach ($args[0][2] as $arg) { + $argLists[] = $this->coerceList($arg); + } + + $lists = []; + $firstList = array_shift($argLists); + + $result = [Type::T_LIST, ',', $lists]; + if (! \is_null($firstList)) { + foreach ($firstList[2] as $key => $item) { + $list = [Type::T_LIST, '', [$item]]; + + foreach ($argLists as $arg) { + if (isset($arg[2][$key])) { + $list[2][] = $arg[2][$key]; + } else { + break 2; + } + } + + $lists[] = $list; + } + + $result[2] = $lists; + } else { + $result['enclosing'] = 'parent'; + } + + return $result; + } + + protected static $libTypeOf = ['value']; + protected function libTypeOf($args) + { + $value = $args[0]; + + return [Type::T_KEYWORD, $this->getTypeOf($value)]; + } + + /** + * @param array|Number $value + * + * @return string + */ + private function getTypeOf($value) + { + switch ($value[0]) { + case Type::T_KEYWORD: + if ($value === static::$true || $value === static::$false) { + return 'bool'; + } + + if ($this->coerceColor($value)) { + return 'color'; + } + + // fall-thru + case Type::T_FUNCTION: + return 'string'; + + case Type::T_FUNCTION_REFERENCE: + return 'function'; + + case Type::T_LIST: + if (isset($value[3]) && \is_array($value[3])) { + return 'arglist'; + } + + // fall-thru + default: + return $value[0]; + } + } + + protected static $libUnit = ['number']; + protected function libUnit($args) + { + $num = $this->assertNumber($args[0], 'number'); + + return [Type::T_STRING, '"', [$num->unitStr()]]; + } + + protected static $libUnitless = ['number']; + protected function libUnitless($args) + { + $value = $this->assertNumber($args[0], 'number'); + + return $this->toBool($value->unitless()); + } + + protected static $libComparable = [ + ['number1', 'number2'], + ['number-1', 'number-2'] + ]; + protected function libComparable($args) + { + list($number1, $number2) = $args; + + if ( + ! $number1 instanceof Number || + ! $number2 instanceof Number + ) { + throw $this->error('Invalid argument(s) for "comparable"'); + } + + return $this->toBool($number1->isComparableTo($number2)); + } + + protected static $libStrIndex = ['string', 'substring']; + protected function libStrIndex($args) + { + $string = $this->assertString($args[0], 'string'); + $stringContent = $this->compileStringContent($string); + + $substring = $this->assertString($args[1], 'substring'); + $substringContent = $this->compileStringContent($substring); + + if (! \strlen($substringContent)) { + $result = 0; + } else { + $result = Util::mbStrpos($stringContent, $substringContent); + } + + return $result === false ? static::$null : new Number($result + 1, ''); + } + + protected static $libStrInsert = ['string', 'insert', 'index']; + protected function libStrInsert($args) + { + $string = $this->assertString($args[0], 'string'); + $stringContent = $this->compileStringContent($string); + + $insert = $this->assertString($args[1], 'insert'); + $insertContent = $this->compileStringContent($insert); + + $index = $this->assertInteger($args[2], 'index'); + if ($index > 0) { + $index = $index - 1; + } + if ($index < 0) { + $index = Util::mbStrlen($stringContent) + 1 + $index; + } + + $string[2] = [ + Util::mbSubstr($stringContent, 0, $index), + $insertContent, + Util::mbSubstr($stringContent, $index) + ]; + + return $string; + } + + protected static $libStrLength = ['string']; + protected function libStrLength($args) + { + $string = $this->assertString($args[0], 'string'); + $stringContent = $this->compileStringContent($string); + + return new Number(Util::mbStrlen($stringContent), ''); + } + + protected static $libStrSlice = ['string', 'start-at', 'end-at:-1']; + protected function libStrSlice($args) + { + $string = $this->assertString($args[0], 'string'); + $stringContent = $this->compileStringContent($string); + + $start = $this->assertNumber($args[1], 'start-at'); + $start->assertNoUnits('start-at'); + $startInt = $this->assertInteger($start, 'start-at'); + $end = $this->assertNumber($args[2], 'end-at'); + $end->assertNoUnits('end-at'); + $endInt = $this->assertInteger($end, 'end-at'); + + if ($endInt === 0) { + return [Type::T_STRING, $string[1], []]; + } + + if ($startInt > 0) { + $startInt--; + } + + if ($endInt < 0) { + $endInt = Util::mbStrlen($stringContent) + $endInt; + } else { + $endInt--; + } + + if ($endInt < $startInt) { + return [Type::T_STRING, $string[1], []]; + } + + $length = $endInt - $startInt + 1; // The end of the slice is inclusive + + $string[2] = [Util::mbSubstr($stringContent, $startInt, $length)]; + + return $string; + } + + protected static $libToLowerCase = ['string']; + protected function libToLowerCase($args) + { + $string = $this->assertString($args[0], 'string'); + $stringContent = $this->compileStringContent($string); + + $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtolower')]; + + return $string; + } + + protected static $libToUpperCase = ['string']; + protected function libToUpperCase($args) + { + $string = $this->assertString($args[0], 'string'); + $stringContent = $this->compileStringContent($string); + + $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtoupper')]; + + return $string; + } + + /** + * Apply a filter on a string content, only on ascii chars + * let extended chars untouched + * + * @param string $stringContent + * @param callable $filter + * @return string + */ + protected function stringTransformAsciiOnly($stringContent, $filter) + { + $mblength = Util::mbStrlen($stringContent); + if ($mblength === strlen($stringContent)) { + return $filter($stringContent); + } + $filteredString = ""; + for ($i = 0; $i < $mblength; $i++) { + $char = Util::mbSubstr($stringContent, $i, 1); + if (strlen($char) > 1) { + $filteredString .= $char; + } else { + $filteredString .= $filter($char); + } + } + + return $filteredString; + } + + protected static $libFeatureExists = ['feature']; + protected function libFeatureExists($args) + { + $string = $this->assertString($args[0], 'feature'); + $name = $this->compileStringContent($string); + + return $this->toBool( + \array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false + ); + } + + protected static $libFunctionExists = ['name']; + protected function libFunctionExists($args) + { + $string = $this->assertString($args[0], 'name'); + $name = $this->compileStringContent($string); + + // user defined functions + if ($this->has(static::$namespaces['function'] . $name)) { + return self::$true; + } + + $name = $this->normalizeName($name); + + if (isset($this->userFunctions[$name])) { + return self::$true; + } + + // built-in functions + $f = $this->getBuiltinFunction($name); + + return $this->toBool(\is_callable($f)); + } + + protected static $libGlobalVariableExists = ['name']; + protected function libGlobalVariableExists($args) + { + $string = $this->assertString($args[0], 'name'); + $name = $this->compileStringContent($string); + + return $this->toBool($this->has($name, $this->rootEnv)); + } + + protected static $libMixinExists = ['name']; + protected function libMixinExists($args) + { + $string = $this->assertString($args[0], 'name'); + $name = $this->compileStringContent($string); + + return $this->toBool($this->has(static::$namespaces['mixin'] . $name)); + } + + protected static $libVariableExists = ['name']; + protected function libVariableExists($args) + { + $string = $this->assertString($args[0], 'name'); + $name = $this->compileStringContent($string); + + return $this->toBool($this->has($name)); + } + + protected static $libCounter = ['args...']; + /** + * Workaround IE7's content counter bug. + * + * @param array $args + * + * @return array + */ + protected function libCounter($args) + { + $list = array_map([$this, 'compileValue'], $args[0][2]); + + return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']]; + } + + protected static $libRandom = ['limit:null']; + protected function libRandom($args) + { + if (isset($args[0]) && $args[0] !== static::$null) { + $n = $this->assertInteger($args[0], 'limit'); + + if ($n < 1) { + throw new SassScriptException("\$limit: Must be greater than 0, was $n."); + } + + return new Number(mt_rand(1, $n), ''); + } + + $max = mt_getrandmax(); + return new Number(mt_rand(0, $max - 1) / $max, ''); + } + + protected static $libUniqueId = []; + protected function libUniqueId() + { + static $id; + + if (! isset($id)) { + $id = PHP_INT_SIZE === 4 + ? mt_rand(0, pow(36, 5)) . str_pad(mt_rand(0, pow(36, 5)) % 10000000, 7, '0', STR_PAD_LEFT) + : mt_rand(0, pow(36, 8)); + } + + $id += mt_rand(0, 10) + 1; + + return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]]; + } + + /** + * @param array|Number $value + * @param bool $force_enclosing_display + * + * @return array + */ + protected function inspectFormatValue($value, $force_enclosing_display = false) + { + if ($value === static::$null) { + $value = [Type::T_KEYWORD, 'null']; + } + + $stringValue = [$value]; + + if ($value instanceof Number) { + return [Type::T_STRING, '', $stringValue]; + } + + if ($value[0] === Type::T_LIST) { + if (end($value[2]) === static::$null) { + array_pop($value[2]); + $value[2][] = [Type::T_STRING, '', ['']]; + $force_enclosing_display = true; + } + + if ( + ! empty($value['enclosing']) && + ($force_enclosing_display || + ($value['enclosing'] === 'bracket') || + ! \count($value[2])) + ) { + $value['enclosing'] = 'forced_' . $value['enclosing']; + $force_enclosing_display = true; + } + + foreach ($value[2] as $k => $listelement) { + $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display); + } + + $stringValue = [$value]; + } + + return [Type::T_STRING, '', $stringValue]; + } + + protected static $libInspect = ['value']; + protected function libInspect($args) + { + $value = $args[0]; + + return $this->inspectFormatValue($value); + } + + /** + * Preprocess selector args + * + * @param array $arg + * @param string|null $varname + * @param bool $allowParent + * + * @return array + */ + protected function getSelectorArg($arg, $varname = null, $allowParent = false) + { + static $parser = null; + + if (\is_null($parser)) { + $parser = $this->parserFactory(__METHOD__); + } + + if (! $this->checkSelectorArgType($arg)) { + $var_value = $this->compileValue($arg); + throw SassScriptException::forArgument("$var_value is not a valid selector: it must be a string, a list of strings, or a list of lists of strings", $varname); + } + + + if ($arg[0] === Type::T_STRING) { + $arg[1] = ''; + } + $arg = $this->compileValue($arg); + + $parsedSelector = []; + + if ($parser->parseSelector($arg, $parsedSelector, true)) { + $selector = $this->evalSelectors($parsedSelector); + $gluedSelector = $this->glueFunctionSelectors($selector); + + if (! $allowParent) { + foreach ($gluedSelector as $selector) { + foreach ($selector as $s) { + if (in_array(static::$selfSelector, $s)) { + throw SassScriptException::forArgument("Parent selectors aren't allowed here.", $varname); + } + } + } + } + + return $gluedSelector; + } + + throw SassScriptException::forArgument("expected more input, invalid selector.", $varname); + } + + /** + * Check variable type for getSelectorArg() function + * @param array $arg + * @param int $maxDepth + * @return bool + */ + protected function checkSelectorArgType($arg, $maxDepth = 2) + { + if ($arg[0] === Type::T_LIST && $maxDepth > 0) { + foreach ($arg[2] as $elt) { + if (! $this->checkSelectorArgType($elt, $maxDepth - 1)) { + return false; + } + } + return true; + } + if (!in_array($arg[0], [Type::T_STRING, Type::T_KEYWORD])) { + return false; + } + return true; + } + + /** + * Postprocess selector to output in right format + * + * @param array $selectors + * + * @return array + */ + protected function formatOutputSelector($selectors) + { + $selectors = $this->collapseSelectorsAsList($selectors); + + return $selectors; + } + + protected static $libIsSuperselector = ['super', 'sub']; + protected function libIsSuperselector($args) + { + list($super, $sub) = $args; + + $super = $this->getSelectorArg($super, 'super'); + $sub = $this->getSelectorArg($sub, 'sub'); + + return $this->toBool($this->isSuperSelector($super, $sub)); + } + + /** + * Test a $super selector again $sub + * + * @param array $super + * @param array $sub + * + * @return boolean + */ + protected function isSuperSelector($super, $sub) + { + // one and only one selector for each arg + if (! $super) { + throw $this->error('Invalid super selector for isSuperSelector()'); + } + + if (! $sub) { + throw $this->error('Invalid sub selector for isSuperSelector()'); + } + + if (count($sub) > 1) { + foreach ($sub as $s) { + if (! $this->isSuperSelector($super, [$s])) { + return false; + } + } + return true; + } + + if (count($super) > 1) { + foreach ($super as $s) { + if ($this->isSuperSelector([$s], $sub)) { + return true; + } + } + return false; + } + + $super = reset($super); + $sub = reset($sub); + + $i = 0; + $nextMustMatch = false; + + foreach ($super as $node) { + $compound = ''; + + array_walk_recursive( + $node, + function ($value, $key) use (&$compound) { + $compound .= $value; + } + ); + + if ($this->isImmediateRelationshipCombinator($compound)) { + if ($node !== $sub[$i]) { + return false; + } + + $nextMustMatch = true; + $i++; + } else { + while ($i < \count($sub) && ! $this->isSuperPart($node, $sub[$i])) { + if ($nextMustMatch) { + return false; + } + + $i++; + } + + if ($i >= \count($sub)) { + return false; + } + + $nextMustMatch = false; + $i++; + } + } + + return true; + } + + /** + * Test a part of super selector again a part of sub selector + * + * @param array $superParts + * @param array $subParts + * + * @return boolean + */ + protected function isSuperPart($superParts, $subParts) + { + $i = 0; + + foreach ($superParts as $superPart) { + while ($i < \count($subParts) && $subParts[$i] !== $superPart) { + $i++; + } + + if ($i >= \count($subParts)) { + return false; + } + + $i++; + } + + return true; + } + + protected static $libSelectorAppend = ['selector...']; + protected function libSelectorAppend($args) + { + // get the selector... list + $args = reset($args); + $args = $args[2]; + + if (\count($args) < 1) { + throw $this->error('selector-append() needs at least 1 argument'); + } + + $selectors = []; + foreach ($args as $arg) { + $selectors[] = $this->getSelectorArg($arg, 'selector'); + } + + return $this->formatOutputSelector($this->selectorAppend($selectors)); + } + + /** + * Append parts of the last selector in the list to the previous, recursively + * + * @param array $selectors + * + * @return array + * + * @throws \ScssPhp\ScssPhp\Exception\CompilerException + */ + protected function selectorAppend($selectors) + { + $lastSelectors = array_pop($selectors); + + if (! $lastSelectors) { + throw $this->error('Invalid selector list in selector-append()'); + } + + while (\count($selectors)) { + $previousSelectors = array_pop($selectors); + + if (! $previousSelectors) { + throw $this->error('Invalid selector list in selector-append()'); + } + + // do the trick, happening $lastSelector to $previousSelector + $appended = []; + + foreach ($lastSelectors as $lastSelector) { + $previous = $previousSelectors; + + foreach ($lastSelector as $lastSelectorParts) { + foreach ($lastSelectorParts as $lastSelectorPart) { + foreach ($previous as $i => $previousSelector) { + foreach ($previousSelector as $j => $previousSelectorParts) { + $previous[$i][$j][] = $lastSelectorPart; + } + } + } + } + + foreach ($previous as $ps) { + $appended[] = $ps; + } + } + + $lastSelectors = $appended; + } + + return $lastSelectors; + } + + protected static $libSelectorExtend = [ + ['selector', 'extendee', 'extender'], + ['selectors', 'extendee', 'extender'] + ]; + protected function libSelectorExtend($args) + { + list($selectors, $extendee, $extender) = $args; + + $selectors = $this->getSelectorArg($selectors, 'selector'); + $extendee = $this->getSelectorArg($extendee, 'extendee'); + $extender = $this->getSelectorArg($extender, 'extender'); + + if (! $selectors || ! $extendee || ! $extender) { + throw $this->error('selector-extend() invalid arguments'); + } + + $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender); + + return $this->formatOutputSelector($extended); + } + + protected static $libSelectorReplace = [ + ['selector', 'original', 'replacement'], + ['selectors', 'original', 'replacement'] + ]; + protected function libSelectorReplace($args) + { + list($selectors, $original, $replacement) = $args; + + $selectors = $this->getSelectorArg($selectors, 'selector'); + $original = $this->getSelectorArg($original, 'original'); + $replacement = $this->getSelectorArg($replacement, 'replacement'); + + if (! $selectors || ! $original || ! $replacement) { + throw $this->error('selector-replace() invalid arguments'); + } + + $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true); + + return $this->formatOutputSelector($replaced); + } + + /** + * Extend/replace in selectors + * used by selector-extend and selector-replace that use the same logic + * + * @param array $selectors + * @param array $extendee + * @param array $extender + * @param boolean $replace + * + * @return array + */ + protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false) + { + $saveExtends = $this->extends; + $saveExtendsMap = $this->extendsMap; + + $this->extends = []; + $this->extendsMap = []; + + foreach ($extendee as $es) { + if (\count($es) !== 1) { + throw $this->error('Can\'t extend complex selector.'); + } + + // only use the first one + $this->pushExtends(reset($es), $extender, null); + } + + $extended = []; + + foreach ($selectors as $selector) { + if (! $replace) { + $extended[] = $selector; + } + + $n = \count($extended); + + $this->matchExtends($selector, $extended); + + // if didnt match, keep the original selector if we are in a replace operation + if ($replace && \count($extended) === $n) { + $extended[] = $selector; + } + } + + $this->extends = $saveExtends; + $this->extendsMap = $saveExtendsMap; + + return $extended; + } + + protected static $libSelectorNest = ['selector...']; + protected function libSelectorNest($args) + { + // get the selector... list + $args = reset($args); + $args = $args[2]; + + if (\count($args) < 1) { + throw $this->error('selector-nest() needs at least 1 argument'); + } + + $selectorsMap = []; + foreach ($args as $arg) { + $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true); + } + + $envs = []; + + foreach ($selectorsMap as $selectors) { + $env = new Environment(); + $env->selectors = $selectors; + + $envs[] = $env; + } + + $envs = array_reverse($envs); + $env = $this->extractEnv($envs); + $outputSelectors = $this->multiplySelectors($env); + + return $this->formatOutputSelector($outputSelectors); + } + + protected static $libSelectorParse = [ + ['selector'], + ['selectors'] + ]; + protected function libSelectorParse($args) + { + $selectors = reset($args); + $selectors = $this->getSelectorArg($selectors, 'selector'); + + return $this->formatOutputSelector($selectors); + } + + protected static $libSelectorUnify = ['selectors1', 'selectors2']; + protected function libSelectorUnify($args) + { + list($selectors1, $selectors2) = $args; + + $selectors1 = $this->getSelectorArg($selectors1, 'selectors1'); + $selectors2 = $this->getSelectorArg($selectors2, 'selectors2'); + + if (! $selectors1 || ! $selectors2) { + throw $this->error('selector-unify() invalid arguments'); + } + + // only consider the first compound of each + $compound1 = reset($selectors1); + $compound2 = reset($selectors2); + + // unify them and that's it + $unified = $this->unifyCompoundSelectors($compound1, $compound2); + + return $this->formatOutputSelector($unified); + } + + /** + * The selector-unify magic as its best + * (at least works as expected on test cases) + * + * @param array $compound1 + * @param array $compound2 + * + * @return array + */ + protected function unifyCompoundSelectors($compound1, $compound2) + { + if (! \count($compound1)) { + return $compound2; + } + + if (! \count($compound2)) { + return $compound1; + } + + // check that last part are compatible + $lastPart1 = array_pop($compound1); + $lastPart2 = array_pop($compound2); + $last = $this->mergeParts($lastPart1, $lastPart2); + + if (! $last) { + return [[]]; + } + + $unifiedCompound = [$last]; + $unifiedSelectors = [$unifiedCompound]; + + // do the rest + while (\count($compound1) || \count($compound2)) { + $part1 = end($compound1); + $part2 = end($compound2); + + if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) { + list($compound2, $part2, $after2) = $match2; + + if ($after2) { + $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2); + } + + $c = $this->mergeParts($part1, $part2); + $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]); + + $part1 = $part2 = null; + + array_pop($compound1); + } + + if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) { + list($compound1, $part1, $after1) = $match1; + + if ($after1) { + $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1); + } + + $c = $this->mergeParts($part2, $part1); + $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]); + + $part1 = $part2 = null; + + array_pop($compound2); + } + + $new = []; + + if ($part1 && $part2) { + array_pop($compound1); + array_pop($compound2); + + $s = $this->prependSelectors($unifiedSelectors, [$part2]); + $new = array_merge($new, $this->prependSelectors($s, [$part1])); + $s = $this->prependSelectors($unifiedSelectors, [$part1]); + $new = array_merge($new, $this->prependSelectors($s, [$part2])); + } elseif ($part1) { + array_pop($compound1); + + $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1])); + } elseif ($part2) { + array_pop($compound2); + + $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2])); + } + + if ($new) { + $unifiedSelectors = $new; + } + } + + return $unifiedSelectors; + } + + /** + * Prepend each selector from $selectors with $parts + * + * @param array $selectors + * @param array $parts + * + * @return array + */ + protected function prependSelectors($selectors, $parts) + { + $new = []; + + foreach ($selectors as $compoundSelector) { + array_unshift($compoundSelector, $parts); + + $new[] = $compoundSelector; + } + + return $new; + } + + /** + * Try to find a matching part in a compound: + * - with same html tag name + * - with some class or id or something in common + * + * @param array $part + * @param array $compound + * + * @return array|false + */ + protected function matchPartInCompound($part, $compound) + { + $partTag = $this->findTagName($part); + $before = $compound; + $after = []; + + // try to find a match by tag name first + while (\count($before)) { + $p = array_pop($before); + + if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) { + return [$before, $p, $after]; + } + + $after[] = $p; + } + + // try again matching a non empty intersection and a compatible tagname + $before = $compound; + $after = []; + + while (\count($before)) { + $p = array_pop($before); + + if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) { + if (\count(array_intersect($part, $p))) { + return [$before, $p, $after]; + } + } + + $after[] = $p; + } + + return false; + } + + /** + * Merge two part list taking care that + * - the html tag is coming first - if any + * - the :something are coming last + * + * @param array $parts1 + * @param array $parts2 + * + * @return array + */ + protected function mergeParts($parts1, $parts2) + { + $tag1 = $this->findTagName($parts1); + $tag2 = $this->findTagName($parts2); + $tag = $this->checkCompatibleTags($tag1, $tag2); + + // not compatible tags + if ($tag === false) { + return []; + } + + if ($tag) { + if ($tag1) { + $parts1 = array_diff($parts1, [$tag1]); + } + + if ($tag2) { + $parts2 = array_diff($parts2, [$tag2]); + } + } + + $mergedParts = array_merge($parts1, $parts2); + $mergedOrderedParts = []; + + foreach ($mergedParts as $part) { + if (strpos($part, ':') === 0) { + $mergedOrderedParts[] = $part; + } + } + + $mergedParts = array_diff($mergedParts, $mergedOrderedParts); + $mergedParts = array_merge($mergedParts, $mergedOrderedParts); + + if ($tag) { + array_unshift($mergedParts, $tag); + } + + return $mergedParts; + } + + /** + * Check the compatibility between two tag names: + * if both are defined they should be identical or one has to be '*' + * + * @param string $tag1 + * @param string $tag2 + * + * @return array|false + */ + protected function checkCompatibleTags($tag1, $tag2) + { + $tags = [$tag1, $tag2]; + $tags = array_unique($tags); + $tags = array_filter($tags); + + if (\count($tags) > 1) { + $tags = array_diff($tags, ['*']); + } + + // not compatible nodes + if (\count($tags) > 1) { + return false; + } + + return $tags; + } + + /** + * Find the html tag name in a selector parts list + * + * @param string[] $parts + * + * @return string + */ + protected function findTagName($parts) + { + foreach ($parts as $part) { + if (! preg_match('/^[\[.:#%_-]/', $part)) { + return $part; + } + } + + return ''; + } + + protected static $libSimpleSelectors = ['selector']; + protected function libSimpleSelectors($args) + { + $selector = reset($args); + $selector = $this->getSelectorArg($selector, 'selector'); + + // remove selectors list layer, keeping the first one + $selector = reset($selector); + + // remove parts list layer, keeping the first part + $part = reset($selector); + + $listParts = []; + + foreach ($part as $p) { + $listParts[] = [Type::T_STRING, '', [$p]]; + } + + return [Type::T_LIST, ',', $listParts]; + } + + protected static $libScssphpGlob = ['pattern']; + protected function libScssphpGlob($args) + { + @trigger_error(sprintf('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0. Register your own alternative through "%s::registerFunction', __CLASS__), E_USER_DEPRECATED); + + $this->logger->warn('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0.', true); + + $string = $this->assertString($args[0], 'pattern'); + $pattern = $this->compileStringContent($string); + $matches = glob($pattern); + $listParts = []; + + foreach ($matches as $match) { + if (! is_file($match)) { + continue; + } + + $listParts[] = [Type::T_STRING, '"', [$match]]; + } + + return [Type::T_LIST, ',', $listParts]; + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Compiler/CachedResult.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Compiler/CachedResult.php new file mode 100644 index 0000000..a662919 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Compiler/CachedResult.php @@ -0,0 +1,77 @@ + + */ + private $parsedFiles; + + /** + * @var array + * @phpstan-var list + */ + private $resolvedImports; + + /** + * @param CompilationResult $result + * @param array $parsedFiles + * @param array $resolvedImports + * + * @phpstan-param list $resolvedImports + */ + public function __construct(CompilationResult $result, array $parsedFiles, array $resolvedImports) + { + $this->result = $result; + $this->parsedFiles = $parsedFiles; + $this->resolvedImports = $resolvedImports; + } + + /** + * @return CompilationResult + */ + public function getResult() + { + return $this->result; + } + + /** + * @return array + */ + public function getParsedFiles() + { + return $this->parsedFiles; + } + + /** + * @return array + * + * @phpstan-return list + */ + public function getResolvedImports() + { + return $this->resolvedImports; + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Compiler/Environment.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Compiler/Environment.php new file mode 100644 index 0000000..306b15a --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Compiler/Environment.php @@ -0,0 +1,48 @@ + + * + * @internal + */ +class Environment +{ + /** + * @var \ScssPhp\ScssPhp\Block|null + */ + public $block; + + /** + * @var \ScssPhp\ScssPhp\Compiler\Environment|null + */ + public $parent; + + /** + * @var array + */ + public $store; + + /** + * @var array + */ + public $storeUnreduced; + + /** + * @var integer + */ + public $depth; +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Exception/CompilerException.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Exception/CompilerException.php new file mode 100644 index 0000000..0b00cf5 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Exception/CompilerException.php @@ -0,0 +1,24 @@ + + * + * @internal + */ +class CompilerException extends \Exception implements SassException +{ +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Exception/ParserException.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Exception/ParserException.php new file mode 100644 index 0000000..00d77ec --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Exception/ParserException.php @@ -0,0 +1,50 @@ + + * + * @internal + */ +class ParserException extends \Exception implements SassException +{ + /** + * @var array + */ + private $sourcePosition; + + /** + * Get source position + * + * @api + */ + public function getSourcePosition() + { + return $this->sourcePosition; + } + + /** + * Set source position + * + * @api + * + * @param array $sourcePosition + */ + public function setSourcePosition($sourcePosition) + { + $this->sourcePosition = $sourcePosition; + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Exception/RangeException.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Exception/RangeException.php new file mode 100644 index 0000000..4be4dee --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Exception/RangeException.php @@ -0,0 +1,24 @@ + + * + * @internal + */ +class RangeException extends \Exception implements SassException +{ +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Exception/SassException.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Exception/SassException.php new file mode 100644 index 0000000..9f62b3c --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Exception/SassException.php @@ -0,0 +1,7 @@ + + * + * @deprecated The Scssphp server should define its own exception instead. + */ +class ServerException extends \Exception implements SassException +{ +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter.php new file mode 100644 index 0000000..cc42ae8 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter.php @@ -0,0 +1,364 @@ + + * + * @internal + */ +abstract class Formatter +{ + /** + * @var integer + */ + public $indentLevel; + + /** + * @var string + */ + public $indentChar; + + /** + * @var string + */ + public $break; + + /** + * @var string + */ + public $open; + + /** + * @var string + */ + public $close; + + /** + * @var string + */ + public $tagSeparator; + + /** + * @var string + */ + public $assignSeparator; + + /** + * @var boolean + */ + public $keepSemicolons; + + /** + * @var \ScssPhp\ScssPhp\Formatter\OutputBlock + */ + protected $currentBlock; + + /** + * @var integer + */ + protected $currentLine; + + /** + * @var integer + */ + protected $currentColumn; + + /** + * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null + */ + protected $sourceMapGenerator; + + /** + * @var string + */ + protected $strippedSemicolon; + + /** + * Initialize formatter + * + * @api + */ + abstract public function __construct(); + + /** + * Return indentation (whitespace) + * + * @return string + */ + protected function indentStr() + { + return ''; + } + + /** + * Return property assignment + * + * @api + * + * @param string $name + * @param mixed $value + * + * @return string + */ + public function property($name, $value) + { + return rtrim($name) . $this->assignSeparator . $value . ';'; + } + + /** + * Return custom property assignment + * differs in that you have to keep spaces in the value as is + * + * @api + * + * @param string $name + * @param mixed $value + * + * @return string + */ + public function customProperty($name, $value) + { + return rtrim($name) . trim($this->assignSeparator) . $value . ';'; + } + + /** + * Output lines inside a block + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return void + */ + protected function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + $glue = $this->break . $inner; + + $this->write($inner . implode($glue, $block->lines)); + + if (! empty($block->children)) { + $this->write($this->break); + } + } + + /** + * Output block selectors + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return void + */ + protected function blockSelectors(OutputBlock $block) + { + assert(! empty($block->selectors)); + + $inner = $this->indentStr(); + + $this->write($inner + . implode($this->tagSeparator, $block->selectors) + . $this->open . $this->break); + } + + /** + * Output block children + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return void + */ + protected function blockChildren(OutputBlock $block) + { + foreach ($block->children as $child) { + $this->block($child); + } + } + + /** + * Output non-empty block + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return void + */ + protected function block(OutputBlock $block) + { + if (empty($block->lines) && empty($block->children)) { + return; + } + + $this->currentBlock = $block; + + $pre = $this->indentStr(); + + if (! empty($block->selectors)) { + $this->blockSelectors($block); + + $this->indentLevel++; + } + + if (! empty($block->lines)) { + $this->blockLines($block); + } + + if (! empty($block->children)) { + $this->blockChildren($block); + } + + if (! empty($block->selectors)) { + $this->indentLevel--; + + if (! $this->keepSemicolons) { + $this->strippedSemicolon = ''; + } + + if (empty($block->children)) { + $this->write($this->break); + } + + $this->write($pre . $this->close . $this->break); + } + } + + /** + * Test and clean safely empty children + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return boolean + */ + protected function testEmptyChildren($block) + { + $isEmpty = empty($block->lines); + + if ($block->children) { + foreach ($block->children as $k => &$child) { + if (! $this->testEmptyChildren($child)) { + $isEmpty = false; + continue; + } + + if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) { + $child->children = []; + $child->selectors = null; + } + } + } + + return $isEmpty; + } + + /** + * Entry point to formatting a block + * + * @api + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree + * @param \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null $sourceMapGenerator Optional source map generator + * + * @return string + */ + public function format(OutputBlock $block, SourceMapGenerator $sourceMapGenerator = null) + { + $this->sourceMapGenerator = null; + + if ($sourceMapGenerator) { + $this->currentLine = 1; + $this->currentColumn = 0; + $this->sourceMapGenerator = $sourceMapGenerator; + } + + $this->testEmptyChildren($block); + + ob_start(); + + $this->block($block); + + $out = ob_get_clean(); + + return $out; + } + + /** + * Output content + * + * @param string $str + * + * @return void + */ + protected function write($str) + { + if (! empty($this->strippedSemicolon)) { + echo $this->strippedSemicolon; + + $this->strippedSemicolon = ''; + } + + /* + * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator + * will be striped for real before a closing, otherwise displayed unchanged starting the next write + */ + if ( + ! $this->keepSemicolons && + $str && + (strpos($str, ';') !== false) && + (substr($str, -1) === ';') + ) { + $str = substr($str, 0, -1); + + $this->strippedSemicolon = ';'; + } + + if ($this->sourceMapGenerator) { + $lines = explode("\n", $str); + $lastLine = array_pop($lines); + + foreach ($lines as $line) { + // If the written line starts is empty, adding a mapping would add it for + // a non-existent column as we are at the end of the line + if ($line !== '') { + $this->sourceMapGenerator->addMapping( + $this->currentLine, + $this->currentColumn, + $this->currentBlock->sourceLine, + //columns from parser are off by one + $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0, + $this->currentBlock->sourceName + ); + } + + $this->currentLine++; + $this->currentColumn = 0; + } + + if ($lastLine !== '') { + $this->sourceMapGenerator->addMapping( + $this->currentLine, + $this->currentColumn, + $this->currentBlock->sourceLine, + //columns from parser are off by one + $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0, + $this->currentBlock->sourceName + ); + } + + $this->currentColumn += \strlen($lastLine); + } + + echo $str; + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compact.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compact.php new file mode 100644 index 0000000..22f2268 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compact.php @@ -0,0 +1,52 @@ + + * + * @deprecated since 1.4.0. Use the Compressed formatter instead. + * + * @internal + */ +class Compact extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + @trigger_error('The Compact formatter is deprecated since 1.4.0. Use the Compressed formatter instead.', E_USER_DEPRECATED); + + $this->indentLevel = 0; + $this->indentChar = ''; + $this->break = ''; + $this->open = ' {'; + $this->close = "}\n\n"; + $this->tagSeparator = ','; + $this->assignSeparator = ':'; + $this->keepSemicolons = true; + } + + /** + * {@inheritdoc} + */ + public function indentStr() + { + return ' '; + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compressed.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compressed.php new file mode 100644 index 0000000..de13c18 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Compressed.php @@ -0,0 +1,85 @@ + + * + * @internal + */ +class Compressed extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = ''; + $this->open = '{'; + $this->close = '}'; + $this->tagSeparator = ','; + $this->assignSeparator = ':'; + $this->keepSemicolons = false; + } + + /** + * {@inheritdoc} + */ + public function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*' && substr($line, 2, 1) !== '!') { + unset($block->lines[$index]); + } elseif (substr($line, 0, 3) === '/*!') { + $block->lines[$index] = '/*' . substr($line, 3); + } + } + + $this->write($inner . implode($glue, $block->lines)); + + if (! empty($block->children)) { + $this->write($this->break); + } + } + + /** + * Output block selectors + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + */ + protected function blockSelectors(OutputBlock $block) + { + assert(! empty($block->selectors)); + + $inner = $this->indentStr(); + + $this->write( + $inner + . implode( + $this->tagSeparator, + str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors) + ) + . $this->open . $this->break + ); + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Crunched.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Crunched.php new file mode 100644 index 0000000..2bc1e92 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Crunched.php @@ -0,0 +1,87 @@ + + * + * @deprecated since 1.4.0. Use the Compressed formatter instead. + * + * @internal + */ +class Crunched extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + @trigger_error('The Crunched formatter is deprecated since 1.4.0. Use the Compressed formatter instead.', E_USER_DEPRECATED); + + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = ''; + $this->open = '{'; + $this->close = '}'; + $this->tagSeparator = ','; + $this->assignSeparator = ':'; + $this->keepSemicolons = false; + } + + /** + * {@inheritdoc} + */ + public function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*') { + unset($block->lines[$index]); + } + } + + $this->write($inner . implode($glue, $block->lines)); + + if (! empty($block->children)) { + $this->write($this->break); + } + } + + /** + * Output block selectors + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + */ + protected function blockSelectors(OutputBlock $block) + { + assert(! empty($block->selectors)); + + $inner = $this->indentStr(); + + $this->write( + $inner + . implode( + $this->tagSeparator, + str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors) + ) + . $this->open . $this->break + ); + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Debug.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Debug.php new file mode 100644 index 0000000..b3f4422 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Debug.php @@ -0,0 +1,127 @@ + + * + * @deprecated since 1.4.0. + * + * @internal + */ +class Debug extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + @trigger_error('The Debug formatter is deprecated since 1.4.0.', E_USER_DEPRECATED); + + $this->indentLevel = 0; + $this->indentChar = ''; + $this->break = "\n"; + $this->open = ' {'; + $this->close = ' }'; + $this->tagSeparator = ', '; + $this->assignSeparator = ': '; + $this->keepSemicolons = true; + } + + /** + * {@inheritdoc} + */ + protected function indentStr() + { + return str_repeat(' ', $this->indentLevel); + } + + /** + * {@inheritdoc} + */ + protected function blockLines(OutputBlock $block) + { + $indent = $this->indentStr(); + + if (empty($block->lines)) { + $this->write("{$indent}block->lines: []\n"); + + return; + } + + foreach ($block->lines as $index => $line) { + $this->write("{$indent}block->lines[{$index}]: $line\n"); + } + } + + /** + * {@inheritdoc} + */ + protected function blockSelectors(OutputBlock $block) + { + $indent = $this->indentStr(); + + if (empty($block->selectors)) { + $this->write("{$indent}block->selectors: []\n"); + + return; + } + + foreach ($block->selectors as $index => $selector) { + $this->write("{$indent}block->selectors[{$index}]: $selector\n"); + } + } + + /** + * {@inheritdoc} + */ + protected function blockChildren(OutputBlock $block) + { + $indent = $this->indentStr(); + + if (empty($block->children)) { + $this->write("{$indent}block->children: []\n"); + + return; + } + + $this->indentLevel++; + + foreach ($block->children as $i => $child) { + $this->block($child); + } + + $this->indentLevel--; + } + + /** + * {@inheritdoc} + */ + protected function block(OutputBlock $block) + { + $indent = $this->indentStr(); + + $this->write("{$indent}block->type: {$block->type}\n" . + "{$indent}block->depth: {$block->depth}\n"); + + $this->currentBlock = $block; + + $this->blockSelectors($block); + $this->blockLines($block); + $this->blockChildren($block); + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Expanded.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Expanded.php new file mode 100644 index 0000000..a280416 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Expanded.php @@ -0,0 +1,70 @@ + + * + * @internal + */ +class Expanded extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = "\n"; + $this->open = ' {'; + $this->close = '}'; + $this->tagSeparator = ', '; + $this->assignSeparator = ': '; + $this->keepSemicolons = true; + } + + /** + * {@inheritdoc} + */ + protected function indentStr() + { + return str_repeat($this->indentChar, $this->indentLevel); + } + + /** + * {@inheritdoc} + */ + protected function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*') { + $block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line); + } + } + + $this->write($inner . implode($glue, $block->lines)); + + if (empty($block->selectors) || ! empty($block->children)) { + $this->write($this->break); + } + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Nested.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Nested.php new file mode 100644 index 0000000..9e72956 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/Nested.php @@ -0,0 +1,236 @@ + + * + * @deprecated since 1.4.0. Use the Expanded formatter instead. + * + * @internal + */ +class Nested extends Formatter +{ + /** + * @var integer + */ + private $depth; + + /** + * {@inheritdoc} + */ + public function __construct() + { + @trigger_error('The Nested formatter is deprecated since 1.4.0. Use the Expanded formatter instead.', E_USER_DEPRECATED); + + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = "\n"; + $this->open = ' {'; + $this->close = ' }'; + $this->tagSeparator = ', '; + $this->assignSeparator = ': '; + $this->keepSemicolons = true; + } + + /** + * {@inheritdoc} + */ + protected function indentStr() + { + $n = $this->depth - 1; + + return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); + } + + /** + * {@inheritdoc} + */ + protected function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*') { + $block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line); + } + } + + $this->write($inner . implode($glue, $block->lines)); + } + + /** + * {@inheritdoc} + */ + protected function block(OutputBlock $block) + { + static $depths; + static $downLevel; + static $closeBlock; + static $previousEmpty; + static $previousHasSelector; + + if ($block->type === 'root') { + $depths = [ 0 ]; + $downLevel = ''; + $closeBlock = ''; + $this->depth = 0; + $previousEmpty = false; + $previousHasSelector = false; + } + + $isMediaOrDirective = \in_array($block->type, [Type::T_DIRECTIVE, Type::T_MEDIA]); + $isSupport = ($block->type === Type::T_DIRECTIVE + && $block->selectors && strpos(implode('', $block->selectors), '@supports') !== false); + + while ($block->depth < end($depths) || ($block->depth == 1 && end($depths) == 1)) { + array_pop($depths); + $this->depth--; + + if ( + ! $this->depth && ($block->depth <= 1 || (! $this->indentLevel && $block->type === Type::T_COMMENT)) && + (($block->selectors && ! $isMediaOrDirective) || $previousHasSelector) + ) { + $downLevel = $this->break; + } + + if (empty($block->lines) && empty($block->children)) { + $previousEmpty = true; + } + } + + if (empty($block->lines) && empty($block->children)) { + return; + } + + $this->currentBlock = $block; + + if (! empty($block->lines) || (! empty($block->children) && ($this->depth < 1 || $isSupport))) { + if ($block->depth > end($depths)) { + if (! $previousEmpty || $this->depth < 1) { + $this->depth++; + + $depths[] = $block->depth; + } else { + // keep the current depth unchanged but take the block depth as a new reference for following blocks + array_pop($depths); + + $depths[] = $block->depth; + } + } + } + + $previousEmpty = ($block->type === Type::T_COMMENT); + $previousHasSelector = false; + + if (! empty($block->selectors)) { + if ($closeBlock) { + $this->write($closeBlock); + $closeBlock = ''; + } + + if ($downLevel) { + $this->write($downLevel); + $downLevel = ''; + } + + $this->blockSelectors($block); + + $this->indentLevel++; + } + + if (! empty($block->lines)) { + if ($closeBlock) { + $this->write($closeBlock); + $closeBlock = ''; + } + + if ($downLevel) { + $this->write($downLevel); + $downLevel = ''; + } + + $this->blockLines($block); + + $closeBlock = $this->break; + } + + if (! empty($block->children)) { + if ($this->depth > 0 && ($isMediaOrDirective || ! $this->hasFlatChild($block))) { + array_pop($depths); + + $this->depth--; + $this->blockChildren($block); + $this->depth++; + + $depths[] = $block->depth; + } else { + $this->blockChildren($block); + } + } + + // reclear to not be spoiled by children if T_DIRECTIVE + if ($block->type === Type::T_DIRECTIVE) { + $previousHasSelector = false; + } + + if (! empty($block->selectors)) { + $this->indentLevel--; + + if (! $this->keepSemicolons) { + $this->strippedSemicolon = ''; + } + + $this->write($this->close); + + $closeBlock = $this->break; + + if ($this->depth > 1 && ! empty($block->children)) { + array_pop($depths); + $this->depth--; + } + + if (! $isMediaOrDirective) { + $previousHasSelector = true; + } + } + + if ($block->type === 'root') { + $this->write($this->break); + } + } + + /** + * Block has flat child + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return boolean + */ + private function hasFlatChild($block) + { + foreach ($block->children as $child) { + if (empty($child->selectors)) { + return true; + } + } + + return false; + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php new file mode 100644 index 0000000..88deb2d --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php @@ -0,0 +1,68 @@ + + * + * @internal + */ +class OutputBlock +{ + /** + * @var string + */ + public $type; + + /** + * @var integer + */ + public $depth; + + /** + * @var array|null + */ + public $selectors; + + /** + * @var string[] + */ + public $lines; + + /** + * @var OutputBlock[] + */ + public $children; + + /** + * @var OutputBlock|null + */ + public $parent; + + /** + * @var string|null + */ + public $sourceName; + + /** + * @var integer|null + */ + public $sourceLine; + + /** + * @var integer|null + */ + public $sourceColumn; +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php new file mode 100644 index 0000000..7c0a2f7 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php @@ -0,0 +1,48 @@ +stream = $stream; + $this->closeOnDestruct = $closeOnDestruct; + } + + /** + * @internal + */ + public function __destruct() + { + if ($this->closeOnDestruct) { + fclose($this->stream); + } + } + + /** + * @inheritDoc + */ + public function warn($message, $deprecation = false) + { + $prefix = ($deprecation ? 'DEPRECATION ' : '') . 'WARNING: '; + + fwrite($this->stream, $prefix . $message . "\n\n"); + } + + /** + * @inheritDoc + */ + public function debug($message) + { + fwrite($this->stream, $message . "\n"); + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Node.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Node.php new file mode 100644 index 0000000..5301937 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Node.php @@ -0,0 +1,43 @@ + + * + * @internal + */ +abstract class Node +{ + /** + * @var string + */ + public $type; + + /** + * @var integer + */ + public $sourceIndex; + + /** + * @var int|null + */ + public $sourceLine; + + /** + * @var int|null + */ + public $sourceColumn; +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Node/Number.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Node/Number.php new file mode 100644 index 0000000..b326906 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Node/Number.php @@ -0,0 +1,804 @@ + + * + * @template-implements \ArrayAccess + */ +class Number extends Node implements \ArrayAccess +{ + const PRECISION = 10; + + /** + * @var integer + * @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore. + */ + public static $precision = self::PRECISION; + + /** + * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/ + * + * @var array + * @phpstan-var array> + */ + protected static $unitTable = [ + 'in' => [ + 'in' => 1, + 'pc' => 6, + 'pt' => 72, + 'px' => 96, + 'cm' => 2.54, + 'mm' => 25.4, + 'q' => 101.6, + ], + 'turn' => [ + 'deg' => 360, + 'grad' => 400, + 'rad' => 6.28318530717958647692528676, // 2 * M_PI + 'turn' => 1, + ], + 's' => [ + 's' => 1, + 'ms' => 1000, + ], + 'Hz' => [ + 'Hz' => 1, + 'kHz' => 0.001, + ], + 'dpi' => [ + 'dpi' => 1, + 'dpcm' => 1 / 2.54, + 'dppx' => 1 / 96, + ], + ]; + + /** + * @var integer|float + */ + private $dimension; + + /** + * @var string[] + * @phpstan-var list + */ + private $numeratorUnits; + + /** + * @var string[] + * @phpstan-var list + */ + private $denominatorUnits; + + /** + * Initialize number + * + * @param integer|float $dimension + * @param string[]|string $numeratorUnits + * @param string[] $denominatorUnits + * + * @phpstan-param list|string $numeratorUnits + * @phpstan-param list $denominatorUnits + */ + public function __construct($dimension, $numeratorUnits, array $denominatorUnits = []) + { + if (is_string($numeratorUnits)) { + $numeratorUnits = $numeratorUnits ? [$numeratorUnits] : []; + } elseif (isset($numeratorUnits['numerator_units'], $numeratorUnits['denominator_units'])) { + // TODO get rid of this once `$number[2]` is not used anymore + $denominatorUnits = $numeratorUnits['denominator_units']; + $numeratorUnits = $numeratorUnits['numerator_units']; + } + + $this->dimension = $dimension; + $this->numeratorUnits = $numeratorUnits; + $this->denominatorUnits = $denominatorUnits; + } + + /** + * @return float|int + */ + public function getDimension() + { + return $this->dimension; + } + + /** + * @return string[] + */ + public function getNumeratorUnits() + { + return $this->numeratorUnits; + } + + /** + * @return string[] + */ + public function getDenominatorUnits() + { + return $this->denominatorUnits; + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + if ($offset === -3) { + return ! \is_null($this->sourceColumn); + } + + if ($offset === -2) { + return ! \is_null($this->sourceLine); + } + + if ( + $offset === -1 || + $offset === 0 || + $offset === 1 || + $offset === 2 + ) { + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + switch ($offset) { + case -3: + return $this->sourceColumn; + + case -2: + return $this->sourceLine; + + case -1: + return $this->sourceIndex; + + case 0: + return Type::T_NUMBER; + + case 1: + return $this->dimension; + + case 2: + return array('numerator_units' => $this->numeratorUnits, 'denominator_units' => $this->denominatorUnits); + } + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + throw new \BadMethodCallException('Number is immutable'); + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + throw new \BadMethodCallException('Number is immutable'); + } + + /** + * Returns true if the number is unitless + * + * @return boolean + */ + public function unitless() + { + return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0; + } + + /** + * Checks whether the number has exactly this unit + * + * @param string $unit + * + * @return bool + */ + public function hasUnit($unit) + { + return \count($this->numeratorUnits) === 1 && \count($this->denominatorUnits) === 0 && $this->numeratorUnits[0] === $unit; + } + + /** + * Returns unit(s) as the product of numerator units divided by the product of denominator units + * + * @return string + */ + public function unitStr() + { + if ($this->unitless()) { + return ''; + } + + return self::getUnitString($this->numeratorUnits, $this->denominatorUnits); + } + + /** + * @param float|int $min + * @param float|int $max + * @param string|null $name + * + * @return float|int + * @throws SassScriptException + */ + public function valueInRange($min, $max, $name = null) + { + try { + return Util::checkRange('', new Range($min, $max), $this); + } catch (RangeException $e) { + throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s', $this, $min, $this->unitStr(), $max), $name); + } + } + + /** + * @param string|null $varName + * + * @return void + */ + public function assertNoUnits($varName = null) + { + if ($this->unitless()) { + return; + } + + throw SassScriptException::forArgument(sprintf('Expected %s to have no units.', $this), $varName); + } + + /** + * @param string $unit + * @param string|null $varName + * + * @return void + */ + public function assertUnit($unit, $varName = null) + { + if ($this->hasUnit($unit)) { + return; + } + + throw SassScriptException::forArgument(sprintf('Expected %s to have unit "%s".', $this, $unit), $varName); + } + + /** + * @param Number $other + * + * @return void + */ + public function assertSameUnitOrUnitless(Number $other) + { + if ($other->unitless()) { + return; + } + + if ($this->numeratorUnits === $other->numeratorUnits && $this->denominatorUnits === $other->denominatorUnits) { + return; + } + + throw new SassScriptException(sprintf( + 'Incompatible units %s and %s.', + self::getUnitString($this->numeratorUnits, $this->denominatorUnits), + self::getUnitString($other->numeratorUnits, $other->denominatorUnits) + )); + } + + /** + * Returns a copy of this number, converted to the units represented by $newNumeratorUnits and $newDenominatorUnits. + * + * This does not throw an error if this number is unitless and + * $newNumeratorUnits/$newDenominatorUnits are not empty, or vice versa. Instead, + * it treats all unitless numbers as convertible to and from all units without + * changing the value. + * + * @param string[] $newNumeratorUnits + * @param string[] $newDenominatorUnits + * + * @return Number + * + * @phpstan-param list $newNumeratorUnits + * @phpstan-param list $newDenominatorUnits + * + * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits + */ + public function coerce(array $newNumeratorUnits, array $newDenominatorUnits) + { + return new Number($this->valueInUnits($newNumeratorUnits, $newDenominatorUnits), $newNumeratorUnits, $newDenominatorUnits); + } + + /** + * @param Number $other + * + * @return bool + */ + public function isComparableTo(Number $other) + { + if ($this->unitless() || $other->unitless()) { + return true; + } + + try { + $this->greaterThan($other); + return true; + } catch (SassScriptException $e) { + return false; + } + } + + /** + * @param Number $other + * + * @return bool + */ + public function lessThan(Number $other) + { + return $this->coerceUnits($other, function ($num1, $num2) { + return $num1 < $num2; + }); + } + + /** + * @param Number $other + * + * @return bool + */ + public function lessThanOrEqual(Number $other) + { + return $this->coerceUnits($other, function ($num1, $num2) { + return $num1 <= $num2; + }); + } + + /** + * @param Number $other + * + * @return bool + */ + public function greaterThan(Number $other) + { + return $this->coerceUnits($other, function ($num1, $num2) { + return $num1 > $num2; + }); + } + + /** + * @param Number $other + * + * @return bool + */ + public function greaterThanOrEqual(Number $other) + { + return $this->coerceUnits($other, function ($num1, $num2) { + return $num1 >= $num2; + }); + } + + /** + * @param Number $other + * + * @return Number + */ + public function plus(Number $other) + { + return $this->coerceNumber($other, function ($num1, $num2) { + return $num1 + $num2; + }); + } + + /** + * @param Number $other + * + * @return Number + */ + public function minus(Number $other) + { + return $this->coerceNumber($other, function ($num1, $num2) { + return $num1 - $num2; + }); + } + + /** + * @return Number + */ + public function unaryMinus() + { + return new Number(-$this->dimension, $this->numeratorUnits, $this->denominatorUnits); + } + + /** + * @param Number $other + * + * @return Number + */ + public function modulo(Number $other) + { + return $this->coerceNumber($other, function ($num1, $num2) { + if ($num2 == 0) { + return NAN; + } + + $result = fmod($num1, $num2); + + if ($result == 0) { + return 0; + } + + if ($num2 < 0 xor $num1 < 0) { + $result += $num2; + } + + return $result; + }); + } + + /** + * @param Number $other + * + * @return Number + */ + public function times(Number $other) + { + return $this->multiplyUnits($this->dimension * $other->dimension, $this->numeratorUnits, $this->denominatorUnits, $other->numeratorUnits, $other->denominatorUnits); + } + + /** + * @param Number $other + * + * @return Number + */ + public function dividedBy(Number $other) + { + if ($other->dimension == 0) { + if ($this->dimension == 0) { + $value = NAN; + } elseif ($this->dimension > 0) { + $value = INF; + } else { + $value = -INF; + } + } else { + $value = $this->dimension / $other->dimension; + } + + return $this->multiplyUnits($value, $this->numeratorUnits, $this->denominatorUnits, $other->denominatorUnits, $other->numeratorUnits); + } + + /** + * @param Number $other + * + * @return bool + */ + public function equals(Number $other) + { + // Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here. + if ($this->unitless() !== $other->unitless()) { + return false; + } + + // In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF + if (is_nan($this->dimension) || is_nan($other->dimension) || !is_finite($this->dimension) || !is_finite($other->dimension)) { + return false; + } + + if ($this->unitless()) { + return round($this->dimension, self::PRECISION) == round($other->dimension, self::PRECISION); + } + + try { + return $this->coerceUnits($other, function ($num1, $num2) { + return round($num1,self::PRECISION) == round($num2, self::PRECISION); + }); + } catch (SassScriptException $e) { + return false; + } + } + + /** + * Output number + * + * @param \ScssPhp\ScssPhp\Compiler $compiler + * + * @return string + */ + public function output(Compiler $compiler = null) + { + $dimension = round($this->dimension, self::PRECISION); + + if (is_nan($dimension)) { + return 'NaN'; + } + + if ($dimension === INF) { + return 'Infinity'; + } + + if ($dimension === -INF) { + return '-Infinity'; + } + + if ($compiler) { + $unit = $this->unitStr(); + } elseif (isset($this->numeratorUnits[0])) { + $unit = $this->numeratorUnits[0]; + } else { + $unit = ''; + } + + $dimension = number_format($dimension, self::PRECISION, '.', ''); + + return rtrim(rtrim($dimension, '0'), '.') . $unit; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->output(); + } + + /** + * @param Number $other + * @param callable $operation + * + * @return Number + * + * @phpstan-param callable(int|float, int|float): (int|float) $operation + */ + private function coerceNumber(Number $other, $operation) + { + $result = $this->coerceUnits($other, $operation); + + if (!$this->unitless()) { + return new Number($result, $this->numeratorUnits, $this->denominatorUnits); + } + + return new Number($result, $other->numeratorUnits, $other->denominatorUnits); + } + + /** + * @param Number $other + * @param callable $operation + * + * @return mixed + * + * @phpstan-template T + * @phpstan-param callable(int|float, int|float): T $operation + * @phpstan-return T + */ + private function coerceUnits(Number $other, $operation) + { + if (!$this->unitless()) { + $num1 = $this->dimension; + $num2 = $other->valueInUnits($this->numeratorUnits, $this->denominatorUnits); + } else { + $num1 = $this->valueInUnits($other->numeratorUnits, $other->denominatorUnits); + $num2 = $other->dimension; + } + + return \call_user_func($operation, $num1, $num2); + } + + /** + * @param string[] $numeratorUnits + * @param string[] $denominatorUnits + * + * @return int|float + * + * @phpstan-param list $numeratorUnits + * @phpstan-param list $denominatorUnits + * + * @throws SassScriptException if this number's units are not compatible with $numeratorUnits and $denominatorUnits + */ + private function valueInUnits(array $numeratorUnits, array $denominatorUnits) + { + if ( + $this->unitless() + || (\count($numeratorUnits) === 0 && \count($denominatorUnits) === 0) + || ($this->numeratorUnits === $numeratorUnits && $this->denominatorUnits === $denominatorUnits) + ) { + return $this->dimension; + } + + $value = $this->dimension; + $oldNumerators = $this->numeratorUnits; + + foreach ($numeratorUnits as $newNumerator) { + foreach ($oldNumerators as $key => $oldNumerator) { + $conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator); + + if (\is_null($conversionFactor)) { + continue; + } + + $value *= $conversionFactor; + unset($oldNumerators[$key]); + continue 2; + } + + throw new SassScriptException(sprintf( + 'Incompatible units %s and %s.', + self::getUnitString($this->numeratorUnits, $this->denominatorUnits), + self::getUnitString($numeratorUnits, $denominatorUnits) + )); + } + + $oldDenominators = $this->denominatorUnits; + + foreach ($denominatorUnits as $newDenominator) { + foreach ($oldDenominators as $key => $oldDenominator) { + $conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator); + + if (\is_null($conversionFactor)) { + continue; + } + + $value /= $conversionFactor; + unset($oldDenominators[$key]); + continue 2; + } + + throw new SassScriptException(sprintf( + 'Incompatible units %s and %s.', + self::getUnitString($this->numeratorUnits, $this->denominatorUnits), + self::getUnitString($numeratorUnits, $denominatorUnits) + )); + } + + if (\count($oldNumerators) || \count($oldDenominators)) { + throw new SassScriptException(sprintf( + 'Incompatible units %s and %s.', + self::getUnitString($this->numeratorUnits, $this->denominatorUnits), + self::getUnitString($numeratorUnits, $denominatorUnits) + )); + } + + return $value; + } + + /** + * @param int|float $value + * @param string[] $numerators1 + * @param string[] $denominators1 + * @param string[] $numerators2 + * @param string[] $denominators2 + * + * @return Number + * + * @phpstan-param list $numerators1 + * @phpstan-param list $denominators1 + * @phpstan-param list $numerators2 + * @phpstan-param list $denominators2 + */ + private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2) + { + $newNumerators = array(); + + foreach ($numerators1 as $numerator) { + foreach ($denominators2 as $key => $denominator) { + $conversionFactor = self::getConversionFactor($numerator, $denominator); + + if (\is_null($conversionFactor)) { + continue; + } + + $value /= $conversionFactor; + unset($denominators2[$key]); + continue 2; + } + + $newNumerators[] = $numerator; + } + + foreach ($numerators2 as $numerator) { + foreach ($denominators1 as $key => $denominator) { + $conversionFactor = self::getConversionFactor($numerator, $denominator); + + if (\is_null($conversionFactor)) { + continue; + } + + $value /= $conversionFactor; + unset($denominators1[$key]); + continue 2; + } + + $newNumerators[] = $numerator; + } + + $newDenominators = array_values(array_merge($denominators1, $denominators2)); + + return new Number($value, $newNumerators, $newDenominators); + } + + /** + * Returns the number of [unit1]s per [unit2]. + * + * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`. + * + * @param string $unit1 + * @param string $unit2 + * + * @return float|int|null + */ + private static function getConversionFactor($unit1, $unit2) + { + if ($unit1 === $unit2) { + return 1; + } + + foreach (static::$unitTable as $unitVariants) { + if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) { + return $unitVariants[$unit1] / $unitVariants[$unit2]; + } + } + + return null; + } + + /** + * Returns unit(s) as the product of numerator units divided by the product of denominator units + * + * @param string[] $numerators + * @param string[] $denominators + * + * @phpstan-param list $numerators + * @phpstan-param list $denominators + * + * @return string + */ + private static function getUnitString(array $numerators, array $denominators) + { + if (!\count($numerators)) { + if (\count($denominators) === 0) { + return 'no units'; + } + + if (\count($denominators) === 1) { + return $denominators[0] . '^-1'; + } + + return '(' . implode('*', $denominators) . ')^-1'; + } + + return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : ''); + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/OutputStyle.php b/user/plugins/admin/vendor/scssphp/scssphp/src/OutputStyle.php new file mode 100644 index 0000000..c284639 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/OutputStyle.php @@ -0,0 +1,9 @@ + + * + * @internal + */ +class Parser +{ + const SOURCE_INDEX = -1; + const SOURCE_LINE = -2; + const SOURCE_COLUMN = -3; + + /** + * @var array + */ + protected static $precedence = [ + '=' => 0, + 'or' => 1, + 'and' => 2, + '==' => 3, + '!=' => 3, + '<=' => 4, + '>=' => 4, + '<' => 4, + '>' => 4, + '+' => 5, + '-' => 5, + '*' => 6, + '/' => 6, + '%' => 6, + ]; + + /** + * @var string + */ + protected static $commentPattern; + /** + * @var string + */ + protected static $operatorPattern; + /** + * @var string + */ + protected static $whitePattern; + + /** + * @var Cache|null + */ + protected $cache; + + private $sourceName; + private $sourceIndex; + /** + * @var array + */ + private $sourcePositions; + /** + * @var array|null + */ + private $charset; + /** + * The current offset in the buffer + * + * @var int + */ + private $count; + /** + * @var Block|null + */ + private $env; + /** + * @var bool + */ + private $inParens; + /** + * @var bool + */ + private $eatWhiteDefault; + /** + * @var bool + */ + private $discardComments; + private $allowVars; + /** + * @var string + */ + private $buffer; + private $utf8; + /** + * @var string|null + */ + private $encoding; + private $patternModifiers; + private $commentsSeen; + + private $cssOnly; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Constructor + * + * @api + * + * @param string|null $sourceName + * @param integer $sourceIndex + * @param string|null $encoding + * @param Cache|null $cache + * @param bool $cssOnly + * @param LoggerInterface|null $logger + */ + public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false, LoggerInterface $logger = null) + { + $this->sourceName = $sourceName ?: '(stdin)'; + $this->sourceIndex = $sourceIndex; + $this->charset = null; + $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8'; + $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais'; + $this->commentsSeen = []; + $this->commentsSeen = []; + $this->allowVars = true; + $this->cssOnly = $cssOnly; + $this->logger = $logger ?: new QuietLogger(); + + if (empty(static::$operatorPattern)) { + static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)'; + + $commentSingle = '\/\/'; + $commentMultiLeft = '\/\*'; + $commentMultiRight = '\*\/'; + + static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight; + static::$whitePattern = $this->utf8 + ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS' + : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS'; + } + + $this->cache = $cache; + } + + /** + * Get source file name + * + * @api + * + * @return string + */ + public function getSourceName() + { + return $this->sourceName; + } + + /** + * Throw parser error + * + * @api + * + * @param string $msg + * + * @phpstan-return never-return + * + * @throws ParserException + * + * @deprecated use "parseError" and throw the exception in the caller instead. + */ + public function throwParseError($msg = 'parse error') + { + @trigger_error( + 'The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead', + E_USER_DEPRECATED + ); + + throw $this->parseError($msg); + } + + /** + * Creates a parser error + * + * @api + * + * @param string $msg + * + * @return ParserException + */ + public function parseError($msg = 'parse error') + { + list($line, $column) = $this->getSourcePosition($this->count); + + $loc = empty($this->sourceName) + ? "line: $line, column: $column" + : "$this->sourceName on line $line, at column $column"; + + if ($this->peek('(.*?)(\n|$)', $m, $this->count)) { + $this->restoreEncoding(); + + $e = new ParserException("$msg: failed at `$m[1]` $loc"); + $e->setSourcePosition([$this->sourceName, $line, $column]); + + return $e; + } + + $this->restoreEncoding(); + + $e = new ParserException("$msg: $loc"); + $e->setSourcePosition([$this->sourceName, $line, $column]); + + return $e; + } + + /** + * Parser buffer + * + * @api + * + * @param string $buffer + * + * @return Block + */ + public function parse($buffer) + { + if ($this->cache) { + $cacheKey = $this->sourceName . ':' . md5($buffer); + $parseOptions = [ + 'charset' => $this->charset, + 'utf8' => $this->utf8, + ]; + $v = $this->cache->getCache('parse', $cacheKey, $parseOptions); + + if (! \is_null($v)) { + return $v; + } + } + + // strip BOM (byte order marker) + if (substr($buffer, 0, 3) === "\xef\xbb\xbf") { + $buffer = substr($buffer, 3); + } + + $this->buffer = rtrim($buffer, "\x00..\x1f"); + $this->count = 0; + $this->env = null; + $this->inParens = false; + $this->eatWhiteDefault = true; + + $this->saveEncoding(); + $this->extractLineNumbers($buffer); + + $this->pushBlock(null); // root block + $this->whitespace(); + $this->pushBlock(null); + $this->popBlock(); + + while ($this->parseChunk()) { + ; + } + + if ($this->count !== \strlen($this->buffer)) { + throw $this->parseError(); + } + + if (! empty($this->env->parent)) { + throw $this->parseError('unclosed block'); + } + + if ($this->charset) { + array_unshift($this->env->children, $this->charset); + } + + $this->restoreEncoding(); + + if ($this->cache) { + $this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions); + } + + return $this->env; + } + + /** + * Parse a value or value list + * + * @api + * + * @param string $buffer + * @param string|array $out + * + * @return boolean + */ + public function parseValue($buffer, &$out) + { + $this->count = 0; + $this->env = null; + $this->inParens = false; + $this->eatWhiteDefault = true; + $this->buffer = (string) $buffer; + + $this->saveEncoding(); + $this->extractLineNumbers($this->buffer); + + $list = $this->valueList($out); + + $this->restoreEncoding(); + + return $list; + } + + /** + * Parse a selector or selector list + * + * @api + * + * @param string $buffer + * @param string|array $out + * @param bool $shouldValidate + * + * @return boolean + */ + public function parseSelector($buffer, &$out, $shouldValidate = true) + { + $this->count = 0; + $this->env = null; + $this->inParens = false; + $this->eatWhiteDefault = true; + $this->buffer = (string) $buffer; + + $this->saveEncoding(); + $this->extractLineNumbers($this->buffer); + + // discard space/comments at the start + $this->discardComments = true; + $this->whitespace(); + $this->discardComments = false; + + $selector = $this->selectors($out); + + $this->restoreEncoding(); + + if ($shouldValidate && $this->count !== strlen($buffer)) { + throw $this->parseError("`" . substr($buffer, $this->count) . "` is not a valid Selector in `$buffer`"); + } + + return $selector; + } + + /** + * Parse a media Query + * + * @api + * + * @param string $buffer + * @param string|array $out + * + * @return boolean + */ + public function parseMediaQueryList($buffer, &$out) + { + $this->count = 0; + $this->env = null; + $this->inParens = false; + $this->eatWhiteDefault = true; + $this->buffer = (string) $buffer; + + $this->saveEncoding(); + $this->extractLineNumbers($this->buffer); + + $isMediaQuery = $this->mediaQueryList($out); + + $this->restoreEncoding(); + + return $isMediaQuery; + } + + /** + * Parse a single chunk off the head of the buffer and append it to the + * current parse environment. + * + * Returns false when the buffer is empty, or when there is an error. + * + * This function is called repeatedly until the entire document is + * parsed. + * + * This parser is most similar to a recursive descent parser. Single + * functions represent discrete grammatical rules for the language, and + * they are able to capture the text that represents those rules. + * + * Consider the function Compiler::keyword(). (All parse functions are + * structured the same.) + * + * The function takes a single reference argument. When calling the + * function it will attempt to match a keyword on the head of the buffer. + * If it is successful, it will place the keyword in the referenced + * argument, advance the position in the buffer, and return true. If it + * fails then it won't advance the buffer and it will return false. + * + * All of these parse functions are powered by Compiler::match(), which behaves + * the same way, but takes a literal regular expression. Sometimes it is + * more convenient to use match instead of creating a new function. + * + * Because of the format of the functions, to parse an entire string of + * grammatical rules, you can chain them together using &&. + * + * But, if some of the rules in the chain succeed before one fails, then + * the buffer position will be left at an invalid state. In order to + * avoid this, Compiler::seek() is used to remember and set buffer positions. + * + * Before parsing a chain, use $s = $this->count to remember the current + * position into $s. Then if a chain fails, use $this->seek($s) to + * go back where we started. + * + * @return boolean + */ + protected function parseChunk() + { + $s = $this->count; + + // the directives + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') { + if ( + $this->literal('@at-root', 8) && + ($this->selectors($selector) || true) && + ($this->map($with) || true) && + (($this->matchChar('(') && + $this->interpolation($with) && + $this->matchChar(')')) || true) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s); + $atRoot->selector = $selector; + $atRoot->with = $with; + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@media', 6) && + $this->mediaQueryList($mediaQueryList) && + $this->matchChar('{', false) + ) { + $media = $this->pushSpecialBlock(Type::T_MEDIA, $s); + $media->queryList = $mediaQueryList[2]; + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@mixin', 6) && + $this->keyword($mixinName) && + ($this->argumentDef($args) || true) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s); + $mixin->name = $mixinName; + $mixin->args = $args; + + return true; + } + + $this->seek($s); + + if ( + ($this->literal('@include', 8) && + $this->keyword($mixinName) && + ($this->matchChar('(') && + ($this->argValues($argValues) || true) && + $this->matchChar(')') || true) && + ($this->end()) || + ($this->literal('using', 5) && + $this->argumentDef($argUsing) && + ($this->end() || $this->matchChar('{') && $hasBlock = true)) || + $this->matchChar('{') && $hasBlock = true) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $child = [ + Type::T_INCLUDE, + $mixinName, + isset($argValues) ? $argValues : null, + null, + isset($argUsing) ? $argUsing : null + ]; + + if (! empty($hasBlock)) { + $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s); + $include->child = $child; + } else { + $this->append($child, $s); + } + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@scssphp-import-once', 20) && + $this->valueList($importPath) && + $this->end() + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + list($line, $column) = $this->getSourcePosition($s); + $file = $this->sourceName; + $this->logger->warn("The \"@scssphp-import-once\" directive is deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true); + + $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@import', 7) && + $this->valueList($importPath) && + $importPath[0] !== Type::T_FUNCTION_CALL && + $this->end() + ) { + if ($this->cssOnly) { + $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s); + $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]); + return true; + } + + $this->append([Type::T_IMPORT, $importPath], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@import', 7) && + $this->url($importPath) && + $this->end() + ) { + if ($this->cssOnly) { + $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s); + $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]); + return true; + } + + $this->append([Type::T_IMPORT, $importPath], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@extend', 7) && + $this->selectors($selectors) && + $this->end() + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + // check for '!flag' + $optional = $this->stripOptionalFlag($selectors); + $this->append([Type::T_EXTEND, $selectors, $optional], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@function', 9) && + $this->keyword($fnName) && + $this->argumentDef($args) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s); + $func->name = $fnName; + $func->args = $args; + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@return', 7) && + ($this->valueList($retVal) || true) && + $this->end() + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@each', 5) && + $this->genericList($varNames, 'variable', ',', false) && + $this->literal('in', 2) && + $this->valueList($list) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $each = $this->pushSpecialBlock(Type::T_EACH, $s); + + foreach ($varNames[2] as $varName) { + $each->vars[] = $varName[1]; + } + + $each->list = $list; + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@while', 6) && + $this->expression($cond) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + while ( + $cond[0] === Type::T_LIST && + ! empty($cond['enclosing']) && + $cond['enclosing'] === 'parent' && + \count($cond[2]) == 1 + ) { + $cond = reset($cond[2]); + } + + $while = $this->pushSpecialBlock(Type::T_WHILE, $s); + $while->cond = $cond; + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@for', 4) && + $this->variable($varName) && + $this->literal('from', 4) && + $this->expression($start) && + ($this->literal('through', 7) || + ($forUntil = true && $this->literal('to', 2))) && + $this->expression($end) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $for = $this->pushSpecialBlock(Type::T_FOR, $s); + $for->var = $varName[1]; + $for->start = $start; + $for->end = $end; + $for->until = isset($forUntil); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@if', 3) && + $this->functionCallArgumentsList($cond, false, '{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $if = $this->pushSpecialBlock(Type::T_IF, $s); + + while ( + $cond[0] === Type::T_LIST && + ! empty($cond['enclosing']) && + $cond['enclosing'] === 'parent' && + \count($cond[2]) == 1 + ) { + $cond = reset($cond[2]); + } + + $if->cond = $cond; + $if->cases = []; + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@debug', 6) && + $this->functionCallArgumentsList($value, false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $this->append([Type::T_DEBUG, $value], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@warn', 5) && + $this->functionCallArgumentsList($value, false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $this->append([Type::T_WARN, $value], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@error', 6) && + $this->functionCallArgumentsList($value, false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $this->append([Type::T_ERROR, $value], $s); + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@content', 8) && + ($this->end() || + $this->matchChar('(') && + $this->argValues($argContent) && + $this->matchChar(')') && + $this->end()) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s); + + return true; + } + + $this->seek($s); + + $last = $this->last(); + + if (isset($last) && $last[0] === Type::T_IF) { + list(, $if) = $last; + + if ($this->literal('@else', 5)) { + if ($this->matchChar('{', false)) { + $else = $this->pushSpecialBlock(Type::T_ELSE, $s); + } elseif ( + $this->literal('if', 2) && + $this->functionCallArgumentsList($cond, false, '{', false) + ) { + $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s); + $else->cond = $cond; + } + + if (isset($else)) { + $else->dontAppend = true; + $if->cases[] = $else; + + return true; + } + } + + $this->seek($s); + } + + // only retain the first @charset directive encountered + if ( + $this->literal('@charset', 8) && + $this->valueList($charset) && + $this->end() + ) { + if (! isset($this->charset)) { + $statement = [Type::T_CHARSET, $charset]; + + list($line, $column) = $this->getSourcePosition($s); + + $statement[static::SOURCE_LINE] = $line; + $statement[static::SOURCE_COLUMN] = $column; + $statement[static::SOURCE_INDEX] = $this->sourceIndex; + + $this->charset = $statement; + } + + return true; + } + + $this->seek($s); + + if ( + $this->literal('@supports', 9) && + ($t1 = $this->supportsQuery($supportQuery)) && + ($t2 = $this->matchChar('{', false)) + ) { + $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); + $directive->name = 'supports'; + $directive->value = $supportQuery; + + return true; + } + + $this->seek($s); + + // doesn't match built in directive, do generic one + if ( + $this->matchChar('@', false) && + $this->mixedKeyword($dirName) && + $this->directiveValue($dirValue, '{') + ) { + if (count($dirName) === 1 && is_string(reset($dirName))) { + $dirName = reset($dirName); + } else { + $dirName = [Type::T_STRING, '', $dirName]; + } + if ($dirName === 'media') { + $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s); + } else { + $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); + $directive->name = $dirName; + } + + if (isset($dirValue)) { + ! $this->cssOnly || ($dirValue = $this->assertPlainCssValid($dirValue)); + $directive->value = $dirValue; + } + + return true; + } + + $this->seek($s); + + // maybe it's a generic blockless directive + if ( + $this->matchChar('@', false) && + $this->mixedKeyword($dirName) && + ! $this->isKnownGenericDirective($dirName) && + ($this->end(false) || ($this->directiveValue($dirValue, '') && $this->end(false))) + ) { + if (\count($dirName) === 1 && \is_string(\reset($dirName))) { + $dirName = \reset($dirName); + } else { + $dirName = [Type::T_STRING, '', $dirName]; + } + if ( + ! empty($this->env->parent) && + $this->env->type && + ! \in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA]) + ) { + $plain = \trim(\substr($this->buffer, $s, $this->count - $s)); + throw $this->parseError( + "Unknown directive `{$plain}` not allowed in `" . $this->env->type . "` block" + ); + } + // blockless directives with a blank line after keeps their blank lines after + // sass-spec compliance purpose + $s = $this->count; + $hasBlankLine = false; + if ($this->match('\s*?\n\s*\n', $out, false)) { + $hasBlankLine = true; + $this->seek($s); + } + $isNotRoot = ! empty($this->env->parent); + $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue, $hasBlankLine, $isNotRoot]], $s); + $this->whitespace(); + + return true; + } + + $this->seek($s); + + return false; + } + + $inCssSelector = null; + if ($this->cssOnly) { + $inCssSelector = (! empty($this->env->parent) && + ! in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA])); + } + // custom properties : right part is static + if (($this->customProperty($name) ) && $this->matchChar(':', false)) { + $start = $this->count; + + // but can be complex and finish with ; or } + foreach ([';','}'] as $ending) { + if ( + $this->openString($ending, $stringValue, '(', ')', false) && + $this->end() + ) { + $end = $this->count; + $value = $stringValue; + + // check if we have only a partial value due to nested [] or { } to take in account + $nestingPairs = [['[', ']'], ['{', '}']]; + + foreach ($nestingPairs as $nestingPair) { + $p = strpos($this->buffer, $nestingPair[0], $start); + + if ($p && $p < $end) { + $this->seek($start); + + if ( + $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) && + $this->end() && + $this->count > $end + ) { + $end = $this->count; + $value = $stringValue; + } + } + } + + $this->seek($end); + $this->append([Type::T_CUSTOM_PROPERTY, $name, $value], $s); + + return true; + } + } + + // TODO: output an error here if nothing found according to sass spec + } + + $this->seek($s); + + // property shortcut + // captures most properties before having to parse a selector + if ( + $this->keyword($name, false) && + $this->literal(': ', 2) && + $this->valueList($value) && + $this->end() + ) { + $name = [Type::T_STRING, '', [$name]]; + $this->append([Type::T_ASSIGN, $name, $value], $s); + + return true; + } + + $this->seek($s); + + // variable assigns + if ( + $this->variable($name) && + $this->matchChar(':') && + $this->valueList($value) && + $this->end() + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + // check for '!flag' + $assignmentFlags = $this->stripAssignmentFlags($value); + $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s); + + return true; + } + + $this->seek($s); + + // opening css block + if ( + $this->selectors($selectors) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false); + + $this->pushBlock($selectors, $s); + + if ($this->eatWhiteDefault) { + $this->whitespace(); + $this->append(null); // collect comments at the beginning if needed + } + + return true; + } + + $this->seek($s); + + // property assign, or nested assign + if ( + $this->propertyName($name) && + $this->matchChar(':') + ) { + $foundSomething = false; + + if ($this->valueList($value)) { + if (empty($this->env->parent)) { + throw $this->parseError('expected "{"'); + } + + $this->append([Type::T_ASSIGN, $name, $value], $s); + $foundSomething = true; + } + + if ($this->matchChar('{', false)) { + ! $this->cssOnly || $this->assertPlainCssValid(false); + + $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s); + $propBlock->prefix = $name; + $propBlock->hasValue = $foundSomething; + + $foundSomething = true; + } elseif ($foundSomething) { + $foundSomething = $this->end(); + } + + if ($foundSomething) { + return true; + } + } + + $this->seek($s); + + // closing a block + if ($this->matchChar('}', false)) { + $block = $this->popBlock(); + + if (! isset($block->type) || $block->type !== Type::T_IF) { + if ($this->env->parent) { + $this->append(null); // collect comments before next statement if needed + } + } + + if (isset($block->type) && $block->type === Type::T_INCLUDE) { + $include = $block->child; + unset($block->child); + $include[3] = $block; + $this->append($include, $s); + } elseif (empty($block->dontAppend)) { + $type = isset($block->type) ? $block->type : Type::T_BLOCK; + $this->append([$type, $block], $s); + } + + // collect comments just after the block closing if needed + if ($this->eatWhiteDefault) { + $this->whitespace(); + + if ($this->env->comments) { + $this->append(null); + } + } + + return true; + } + + // extra stuff + if ($this->matchChar(';')) { + return true; + } + + return false; + } + + /** + * Push block onto parse tree + * + * @param array|null $selectors + * @param integer $pos + * + * @return Block + */ + protected function pushBlock($selectors, $pos = 0) + { + list($line, $column) = $this->getSourcePosition($pos); + + $b = new Block(); + $b->sourceName = $this->sourceName; + $b->sourceLine = $line; + $b->sourceColumn = $column; + $b->sourceIndex = $this->sourceIndex; + $b->selectors = $selectors; + $b->comments = []; + $b->parent = $this->env; + + if (! $this->env) { + $b->children = []; + } elseif (empty($this->env->children)) { + $this->env->children = $this->env->comments; + $b->children = []; + $this->env->comments = []; + } else { + $b->children = $this->env->comments; + $this->env->comments = []; + } + + $this->env = $b; + + // collect comments at the beginning of a block if needed + if ($this->eatWhiteDefault) { + $this->whitespace(); + + if ($this->env->comments) { + $this->append(null); + } + } + + return $b; + } + + /** + * Push special (named) block onto parse tree + * + * @param string $type + * @param integer $pos + * + * @return Block + */ + protected function pushSpecialBlock($type, $pos) + { + $block = $this->pushBlock(null, $pos); + $block->type = $type; + + return $block; + } + + /** + * Pop scope and return last block + * + * @return Block + * + * @throws \Exception + */ + protected function popBlock() + { + + // collect comments ending just before of a block closing + if ($this->env->comments) { + $this->append(null); + } + + // pop the block + $block = $this->env; + + if (empty($block->parent)) { + throw $this->parseError('unexpected }'); + } + + if ($block->type == Type::T_AT_ROOT) { + // keeps the parent in case of self selector & + $block->selfParent = $block->parent; + } + + $this->env = $block->parent; + + unset($block->parent); + + return $block; + } + + /** + * Peek input stream + * + * @param string $regex + * @param array $out + * @param integer $from + * + * @return integer + */ + protected function peek($regex, &$out, $from = null) + { + if (! isset($from)) { + $from = $this->count; + } + + $r = '/' . $regex . '/' . $this->patternModifiers; + $result = preg_match($r, $this->buffer, $out, 0, $from); + + return $result; + } + + /** + * Seek to position in input stream (or return current position in input stream) + * + * @param integer $where + */ + protected function seek($where) + { + $this->count = $where; + } + + /** + * Assert a parsed part is plain CSS Valid + * + * @param array|false $parsed + * @param int $startPos + * @throws ParserException + */ + protected function assertPlainCssValid($parsed, $startPos = null) + { + $type = ''; + if ($parsed) { + $type = $parsed[0]; + $parsed = $this->isPlainCssValidElement($parsed); + } + if (! $parsed) { + if (! \is_null($startPos)) { + $plain = rtrim(substr($this->buffer, $startPos, $this->count - $startPos)); + $message = "Error : `{$plain}` isn't allowed in plain CSS"; + } else { + $message = 'Error: SCSS syntax not allowed in CSS file'; + } + if ($type) { + $message .= " ($type)"; + } + throw $this->parseError($message); + } + + return $parsed; + } + + /** + * Check a parsed element is plain CSS Valid + * @param array $parsed + * @return bool|array + */ + protected function isPlainCssValidElement($parsed, $allowExpression = false) + { + // keep string as is + if (is_string($parsed)) { + return $parsed; + } + + if ( + \in_array($parsed[0], [Type::T_FUNCTION, Type::T_FUNCTION_CALL]) && + !\in_array($parsed[1], [ + 'alpha', + 'attr', + 'calc', + 'cubic-bezier', + 'env', + 'grayscale', + 'hsl', + 'hsla', + 'hwb', + 'invert', + 'linear-gradient', + 'min', + 'max', + 'radial-gradient', + 'repeating-linear-gradient', + 'repeating-radial-gradient', + 'rgb', + 'rgba', + 'rotate', + 'saturate', + 'var', + ]) && + Compiler::isNativeFunction($parsed[1]) + ) { + return false; + } + + switch ($parsed[0]) { + case Type::T_BLOCK: + case Type::T_KEYWORD: + case Type::T_NULL: + case Type::T_NUMBER: + case Type::T_MEDIA: + return $parsed; + + case Type::T_COMMENT: + if (isset($parsed[2])) { + return false; + } + return $parsed; + + case Type::T_DIRECTIVE: + if (\is_array($parsed[1])) { + $parsed[1][1] = $this->isPlainCssValidElement($parsed[1][1]); + if (! $parsed[1][1]) { + return false; + } + } + + return $parsed; + + case Type::T_IMPORT: + if ($parsed[1][0] === Type::T_LIST) { + return false; + } + $parsed[1] = $this->isPlainCssValidElement($parsed[1]); + if ($parsed[1] === false) { + return false; + } + return $parsed; + + case Type::T_STRING: + foreach ($parsed[2] as $k => $substr) { + if (\is_array($substr)) { + $parsed[2][$k] = $this->isPlainCssValidElement($substr); + if (! $parsed[2][$k]) { + return false; + } + } + } + return $parsed; + + case Type::T_LIST: + if (!empty($parsed['enclosing'])) { + return false; + } + foreach ($parsed[2] as $k => $listElement) { + $parsed[2][$k] = $this->isPlainCssValidElement($listElement); + if (! $parsed[2][$k]) { + return false; + } + } + return $parsed; + + case Type::T_ASSIGN: + foreach ([1, 2, 3] as $k) { + if (! empty($parsed[$k])) { + $parsed[$k] = $this->isPlainCssValidElement($parsed[$k]); + if (! $parsed[$k]) { + return false; + } + } + } + return $parsed; + + case Type::T_EXPRESSION: + list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed; + if (! $allowExpression && ! \in_array($op, ['and', 'or', '/'])) { + return false; + } + $lhs = $this->isPlainCssValidElement($lhs, true); + if (! $lhs) { + return false; + } + $rhs = $this->isPlainCssValidElement($rhs, true); + if (! $rhs) { + return false; + } + + return [ + Type::T_STRING, + '', [ + $this->inParens ? '(' : '', + $lhs, + ($whiteBefore ? ' ' : '') . $op . ($whiteAfter ? ' ' : ''), + $rhs, + $this->inParens ? ')' : '' + ] + ]; + + case Type::T_CUSTOM_PROPERTY: + case Type::T_UNARY: + $parsed[2] = $this->isPlainCssValidElement($parsed[2]); + if (! $parsed[2]) { + return false; + } + return $parsed; + + case Type::T_FUNCTION: + $argsList = $parsed[2]; + foreach ($argsList[2] as $argElement) { + if (! $this->isPlainCssValidElement($argElement)) { + return false; + } + } + return $parsed; + + case Type::T_FUNCTION_CALL: + $parsed[0] = Type::T_FUNCTION; + $argsList = [Type::T_LIST, ',', []]; + foreach ($parsed[2] as $arg) { + if ($arg[0] || ! empty($arg[2])) { + // no named arguments possible in a css function call + // nor ... argument + return false; + } + $arg = $this->isPlainCssValidElement($arg[1], $parsed[1] === 'calc'); + if (! $arg) { + return false; + } + $argsList[2][] = $arg; + } + $parsed[2] = $argsList; + return $parsed; + } + + return false; + } + + /** + * Match string looking for either ending delim, escape, or string interpolation + * + * {@internal This is a workaround for preg_match's 250K string match limit. }} + * + * @param array $m Matches (passed by reference) + * @param string $delim Delimiter + * + * @return boolean True if match; false otherwise + */ + protected function matchString(&$m, $delim) + { + $token = null; + + $end = \strlen($this->buffer); + + // look for either ending delim, escape, or string interpolation + foreach (['#{', '\\', "\r", $delim] as $lookahead) { + $pos = strpos($this->buffer, $lookahead, $this->count); + + if ($pos !== false && $pos < $end) { + $end = $pos; + $token = $lookahead; + } + } + + if (! isset($token)) { + return false; + } + + $match = substr($this->buffer, $this->count, $end - $this->count); + $m = [ + $match . $token, + $match, + $token + ]; + $this->count = $end + \strlen($token); + + return true; + } + + /** + * Try to match something on head of buffer + * + * @param string $regex + * @param array $out + * @param boolean $eatWhitespace + * + * @return boolean + */ + protected function match($regex, &$out, $eatWhitespace = null) + { + $r = '/' . $regex . '/' . $this->patternModifiers; + + if (! preg_match($r, $this->buffer, $out, 0, $this->count)) { + return false; + } + + $this->count += \strlen($out[0]); + + if (! isset($eatWhitespace)) { + $eatWhitespace = $this->eatWhiteDefault; + } + + if ($eatWhitespace) { + $this->whitespace(); + } + + return true; + } + + /** + * Match a single string + * + * @param string $char + * @param boolean $eatWhitespace + * + * @return boolean + */ + protected function matchChar($char, $eatWhitespace = null) + { + if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) { + return false; + } + + $this->count++; + + if (! isset($eatWhitespace)) { + $eatWhitespace = $this->eatWhiteDefault; + } + + if ($eatWhitespace) { + $this->whitespace(); + } + + return true; + } + + /** + * Match literal string + * + * @param string $what + * @param integer $len + * @param boolean $eatWhitespace + * + * @return boolean + */ + protected function literal($what, $len, $eatWhitespace = null) + { + if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) { + return false; + } + + $this->count += $len; + + if (! isset($eatWhitespace)) { + $eatWhitespace = $this->eatWhiteDefault; + } + + if ($eatWhitespace) { + $this->whitespace(); + } + + return true; + } + + /** + * Match some whitespace + * + * @return boolean + */ + protected function whitespace() + { + $gotWhite = false; + + while (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) { + if (isset($m[1]) && empty($this->commentsSeen[$this->count])) { + // comment that are kept in the output CSS + $comment = []; + $startCommentCount = $this->count; + $endCommentCount = $this->count + \strlen($m[1]); + + // find interpolations in comment + $p = strpos($this->buffer, '#{', $this->count); + + while ($p !== false && $p < $endCommentCount) { + $c = substr($this->buffer, $this->count, $p - $this->count); + $comment[] = $c; + $this->count = $p; + $out = null; + + if ($this->interpolation($out)) { + // keep right spaces in the following string part + if ($out[3]) { + while ($this->buffer[$this->count - 1] !== '}') { + $this->count--; + } + + $out[3] = ''; + } + + $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out]; + } else { + list($line, $column) = $this->getSourcePosition($this->count); + $file = $this->sourceName; + if (!$this->discardComments) { + $this->logger->warn("Unterminated interpolations in multiline comments are deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true); + } + $comment[] = substr($this->buffer, $this->count, 2); + + $this->count += 2; + } + + $p = strpos($this->buffer, '#{', $this->count); + } + + // remaining part + $c = substr($this->buffer, $this->count, $endCommentCount - $this->count); + + if (! $comment) { + // single part static comment + $this->appendComment([Type::T_COMMENT, $c]); + } else { + $comment[] = $c; + $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount); + $commentStatement = [Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]; + + list($line, $column) = $this->getSourcePosition($startCommentCount); + $commentStatement[self::SOURCE_LINE] = $line; + $commentStatement[self::SOURCE_COLUMN] = $column; + $commentStatement[self::SOURCE_INDEX] = $this->sourceIndex; + + $this->appendComment($commentStatement); + } + + $this->commentsSeen[$startCommentCount] = true; + $this->count = $endCommentCount; + } else { + // comment that are ignored and not kept in the output css + $this->count += \strlen($m[0]); + // silent comments are not allowed in plain CSS files + ! $this->cssOnly + || ! \strlen(trim($m[0])) + || $this->assertPlainCssValid(false, $this->count - \strlen($m[0])); + } + + $gotWhite = true; + } + + return $gotWhite; + } + + /** + * Append comment to current block + * + * @param array $comment + */ + protected function appendComment($comment) + { + if (! $this->discardComments) { + $this->env->comments[] = $comment; + } + } + + /** + * Append statement to current block + * + * @param array|null $statement + * @param integer $pos + */ + protected function append($statement, $pos = null) + { + if (! \is_null($statement)) { + ! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos)); + + if (! \is_null($pos)) { + list($line, $column) = $this->getSourcePosition($pos); + + $statement[static::SOURCE_LINE] = $line; + $statement[static::SOURCE_COLUMN] = $column; + $statement[static::SOURCE_INDEX] = $this->sourceIndex; + } + + $this->env->children[] = $statement; + } + + $comments = $this->env->comments; + + if ($comments) { + $this->env->children = array_merge($this->env->children, $comments); + $this->env->comments = []; + } + } + + /** + * Returns last child was appended + * + * @return array|null + */ + protected function last() + { + $i = \count($this->env->children) - 1; + + if (isset($this->env->children[$i])) { + return $this->env->children[$i]; + } + } + + /** + * Parse media query list + * + * @param array $out + * + * @return boolean + */ + protected function mediaQueryList(&$out) + { + return $this->genericList($out, 'mediaQuery', ',', false); + } + + /** + * Parse media query + * + * @param array $out + * + * @return boolean + */ + protected function mediaQuery(&$out) + { + $expressions = null; + $parts = []; + + if ( + ($this->literal('only', 4) && ($only = true) || + $this->literal('not', 3) && ($not = true) || true) && + $this->mixedKeyword($mediaType) + ) { + $prop = [Type::T_MEDIA_TYPE]; + + if (isset($only)) { + $prop[] = [Type::T_KEYWORD, 'only']; + } + + if (isset($not)) { + $prop[] = [Type::T_KEYWORD, 'not']; + } + + $media = [Type::T_LIST, '', []]; + + foreach ((array) $mediaType as $type) { + if (\is_array($type)) { + $media[2][] = $type; + } else { + $media[2][] = [Type::T_KEYWORD, $type]; + } + } + + $prop[] = $media; + $parts[] = $prop; + } + + if (empty($parts) || $this->literal('and', 3)) { + $this->genericList($expressions, 'mediaExpression', 'and', false); + + if (\is_array($expressions)) { + $parts = array_merge($parts, $expressions[2]); + } + } + + $out = $parts; + + return true; + } + + /** + * Parse supports query + * + * @param array $out + * + * @return boolean + */ + protected function supportsQuery(&$out) + { + $expressions = null; + $parts = []; + + $s = $this->count; + + $not = false; + + if ( + ($this->literal('not', 3) && ($not = true) || true) && + $this->matchChar('(') && + ($this->expression($property)) && + $this->literal(': ', 2) && + $this->valueList($value) && + $this->matchChar(')') + ) { + $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]]; + $support[2][] = $property; + $support[2][] = [Type::T_KEYWORD, ': ']; + $support[2][] = $value; + $support[2][] = [Type::T_KEYWORD, ')']; + + $parts[] = $support; + $s = $this->count; + } else { + $this->seek($s); + } + + if ( + $this->matchChar('(') && + $this->supportsQuery($subQuery) && + $this->matchChar(')') + ) { + $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]]; + $s = $this->count; + } else { + $this->seek($s); + } + + if ( + $this->literal('not', 3) && + $this->supportsQuery($subQuery) + ) { + $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]]; + $s = $this->count; + } else { + $this->seek($s); + } + + if ( + $this->literal('selector(', 9) && + $this->selector($selector) && + $this->matchChar(')') + ) { + $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]]; + + $selectorList = [Type::T_LIST, '', []]; + + foreach ($selector as $sc) { + $compound = [Type::T_STRING, '', []]; + + foreach ($sc as $scp) { + if (\is_array($scp)) { + $compound[2][] = $scp; + } else { + $compound[2][] = [Type::T_KEYWORD, $scp]; + } + } + + $selectorList[2][] = $compound; + } + + $support[2][] = $selectorList; + $support[2][] = [Type::T_KEYWORD, ')']; + $parts[] = $support; + $s = $this->count; + } else { + $this->seek($s); + } + + if ($this->variable($var) or $this->interpolation($var)) { + $parts[] = $var; + $s = $this->count; + } else { + $this->seek($s); + } + + if ( + $this->literal('and', 3) && + $this->genericList($expressions, 'supportsQuery', ' and', false) + ) { + array_unshift($expressions[2], [Type::T_STRING, '', $parts]); + + $parts = [$expressions]; + $s = $this->count; + } else { + $this->seek($s); + } + + if ( + $this->literal('or', 2) && + $this->genericList($expressions, 'supportsQuery', ' or', false) + ) { + array_unshift($expressions[2], [Type::T_STRING, '', $parts]); + + $parts = [$expressions]; + $s = $this->count; + } else { + $this->seek($s); + } + + if (\count($parts)) { + if ($this->eatWhiteDefault) { + $this->whitespace(); + } + + $out = [Type::T_STRING, '', $parts]; + + return true; + } + + return false; + } + + + /** + * Parse media expression + * + * @param array $out + * + * @return boolean + */ + protected function mediaExpression(&$out) + { + $s = $this->count; + $value = null; + + if ( + $this->matchChar('(') && + $this->expression($feature) && + ($this->matchChar(':') && + $this->expression($value) || true) && + $this->matchChar(')') + ) { + $out = [Type::T_MEDIA_EXPRESSION, $feature]; + + if ($value) { + $out[] = $value; + } + + return true; + } + + $this->seek($s); + + return false; + } + + /** + * Parse argument values + * + * @param array $out + * + * @return boolean + */ + protected function argValues(&$out) + { + $discardComments = $this->discardComments; + $this->discardComments = true; + + if ($this->genericList($list, 'argValue', ',', false)) { + $out = $list[2]; + + $this->discardComments = $discardComments; + + return true; + } + + $this->discardComments = $discardComments; + + return false; + } + + /** + * Parse argument value + * + * @param array $out + * + * @return boolean + */ + protected function argValue(&$out) + { + $s = $this->count; + + $keyword = null; + + if (! $this->variable($keyword) || ! $this->matchChar(':')) { + $this->seek($s); + + $keyword = null; + } + + if ($this->genericList($value, 'expression', '', true)) { + $out = [$keyword, $value, false]; + $s = $this->count; + + if ($this->literal('...', 3)) { + $out[2] = true; + } else { + $this->seek($s); + } + + return true; + } + + return false; + } + + /** + * Check if a generic directive is known to be able to allow almost any syntax or not + * @param mixed $directiveName + * @return bool + */ + protected function isKnownGenericDirective($directiveName) + { + if (\is_array($directiveName) && \is_string(reset($directiveName))) { + $directiveName = reset($directiveName); + } + if (! \is_string($directiveName)) { + return false; + } + if ( + \in_array($directiveName, [ + 'at-root', + 'media', + 'mixin', + 'include', + 'scssphp-import-once', + 'import', + 'extend', + 'function', + 'break', + 'continue', + 'return', + 'each', + 'while', + 'for', + 'if', + 'debug', + 'warn', + 'error', + 'content', + 'else', + 'charset', + 'supports', + // Todo + 'use', + 'forward', + ]) + ) { + return true; + } + return false; + } + + /** + * Parse directive value list that considers $vars as keyword + * + * @param array $out + * @param boolean|string $endChar + * + * @return boolean + */ + protected function directiveValue(&$out, $endChar = false) + { + $s = $this->count; + + if ($this->variable($out)) { + if ($endChar && $this->matchChar($endChar, false)) { + return true; + } + + if (! $endChar && $this->end()) { + return true; + } + } + + $this->seek($s); + + if (\is_string($endChar) && $this->openString($endChar ? $endChar : ';', $out, null, null, true, ";}{")) { + if ($endChar && $this->matchChar($endChar, false)) { + return true; + } + $ss = $this->count; + if (!$endChar && $this->end()) { + $this->seek($ss); + return true; + } + } + + $this->seek($s); + + $allowVars = $this->allowVars; + $this->allowVars = false; + + $res = $this->genericList($out, 'spaceList', ','); + $this->allowVars = $allowVars; + + if ($res) { + if ($endChar && $this->matchChar($endChar, false)) { + return true; + } + + if (! $endChar && $this->end()) { + return true; + } + } + + $this->seek($s); + + if ($endChar && $this->matchChar($endChar, false)) { + return true; + } + + return false; + } + + /** + * Parse comma separated value list + * + * @param array $out + * + * @return boolean + */ + protected function valueList(&$out) + { + $discardComments = $this->discardComments; + $this->discardComments = true; + $res = $this->genericList($out, 'spaceList', ','); + $this->discardComments = $discardComments; + + return $res; + } + + /** + * Parse a function call, where externals () are part of the call + * and not of the value list + * + * @param $out + * @param bool $mandatoryEnclos + * @param null|string $charAfter + * @param null|bool $eatWhiteSp + * @return bool + */ + protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null) + { + $s = $this->count; + + if ( + $this->matchChar('(') && + $this->valueList($out) && + $this->matchChar(')') && + ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end()) + ) { + return true; + } + + if (! $mandatoryEnclos) { + $this->seek($s); + + if ( + $this->valueList($out) && + ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end()) + ) { + return true; + } + } + + $this->seek($s); + + return false; + } + + /** + * Parse space separated value list + * + * @param array $out + * + * @return boolean + */ + protected function spaceList(&$out) + { + return $this->genericList($out, 'expression'); + } + + /** + * Parse generic list + * + * @param array $out + * @param string $parseItem The name of the method used to parse items + * @param string $delim + * @param boolean $flatten + * + * @return boolean + */ + protected function genericList(&$out, $parseItem, $delim = '', $flatten = true) + { + $s = $this->count; + $items = []; + $value = null; + + while ($this->$parseItem($value)) { + $trailing_delim = false; + $items[] = $value; + + if ($delim) { + if (! $this->literal($delim, \strlen($delim))) { + break; + } + + $trailing_delim = true; + } else { + // if no delim watch that a keyword didn't eat the single/double quote + // from the following starting string + if ($value[0] === Type::T_KEYWORD) { + $word = $value[1]; + + $last_char = substr($word, -1); + + if ( + strlen($word) > 1 && + in_array($last_char, [ "'", '"']) && + substr($word, -2, 1) !== '\\' + ) { + // if there is a non escaped opening quote in the keyword, this seems unlikely a mistake + $word = str_replace('\\' . $last_char, '\\\\', $word); + if (strpos($word, $last_char) < strlen($word) - 1) { + continue; + } + + $currentCount = $this->count; + + // let's try to rewind to previous char and try a parse + $this->count--; + // in case the keyword also eat spaces + while (substr($this->buffer, $this->count, 1) !== $last_char) { + $this->count--; + } + + $nextValue = null; + if ($this->$parseItem($nextValue)) { + if ($nextValue[0] === Type::T_KEYWORD && $nextValue[1] === $last_char) { + // bad try, forget it + $this->seek($currentCount); + continue; + } + if ($nextValue[0] !== Type::T_STRING) { + // bad try, forget it + $this->seek($currentCount); + continue; + } + + // OK it was a good idea + $value[1] = substr($value[1], 0, -1); + array_pop($items); + $items[] = $value; + $items[] = $nextValue; + } else { + // bad try, forget it + $this->seek($currentCount); + continue; + } + } + } + } + } + + if (! $items) { + $this->seek($s); + + return false; + } + + if ($trailing_delim) { + $items[] = [Type::T_NULL]; + } + + if ($flatten && \count($items) === 1) { + $out = $items[0]; + } else { + $out = [Type::T_LIST, $delim, $items]; + } + + return true; + } + + /** + * Parse expression + * + * @param array $out + * @param boolean $listOnly + * @param boolean $lookForExp + * + * @return boolean + */ + protected function expression(&$out, $listOnly = false, $lookForExp = true) + { + $s = $this->count; + $discard = $this->discardComments; + $this->discardComments = true; + $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]); + + if ($this->matchChar('(')) { + if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) { + if ($lookForExp) { + $out = $this->expHelper($lhs, 0); + } else { + $out = $lhs; + } + + $this->discardComments = $discard; + + return true; + } + + $this->seek($s); + } + + if (\in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) { + if ($this->enclosedExpression($lhs, $s, ']', [Type::T_LIST])) { + if ($lookForExp) { + $out = $this->expHelper($lhs, 0); + } else { + $out = $lhs; + } + + $this->discardComments = $discard; + + return true; + } + + $this->seek($s); + } + + if (! $listOnly && $this->value($lhs)) { + if ($lookForExp) { + $out = $this->expHelper($lhs, 0); + } else { + $out = $lhs; + } + + $this->discardComments = $discard; + + return true; + } + + $this->discardComments = $discard; + + return false; + } + + /** + * Parse expression specifically checking for lists in parenthesis or brackets + * + * @param array $out + * @param integer $s + * @param string $closingParen + * @param array $allowedTypes + * + * @return boolean + */ + protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type::T_LIST, Type::T_MAP]) + { + if ($this->matchChar($closingParen) && \in_array(Type::T_LIST, $allowedTypes)) { + $out = [Type::T_LIST, '', []]; + + switch ($closingParen) { + case ')': + $out['enclosing'] = 'parent'; // parenthesis list + break; + + case ']': + $out['enclosing'] = 'bracket'; // bracketed list + break; + } + + return true; + } + + if ( + $this->valueList($out) && + $this->matchChar($closingParen) && ! ($closingParen === ')' && + \in_array($out[0], [Type::T_EXPRESSION, Type::T_UNARY])) && + \in_array(Type::T_LIST, $allowedTypes) + ) { + if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) { + $out = [Type::T_LIST, '', [$out]]; + } + + switch ($closingParen) { + case ')': + $out['enclosing'] = 'parent'; // parenthesis list + break; + + case ']': + $out['enclosing'] = 'bracket'; // bracketed list + break; + } + + return true; + } + + $this->seek($s); + + if (\in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) { + return true; + } + + return false; + } + + /** + * Parse left-hand side of subexpression + * + * @param array $lhs + * @param integer $minP + * + * @return array + */ + protected function expHelper($lhs, $minP) + { + $operators = static::$operatorPattern; + + $ss = $this->count; + $whiteBefore = isset($this->buffer[$this->count - 1]) && + ctype_space($this->buffer[$this->count - 1]); + + while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) { + $whiteAfter = isset($this->buffer[$this->count]) && + ctype_space($this->buffer[$this->count]); + $varAfter = isset($this->buffer[$this->count]) && + $this->buffer[$this->count] === '$'; + + $this->whitespace(); + + $op = $m[1]; + + // don't turn negative numbers into expressions + if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) { + break; + } + + if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) { + break; + } + + if ($op === '-' && ! $whiteAfter && $rhs[0] === Type::T_KEYWORD) { + break; + } + + // consume higher-precedence operators on the right-hand side + $rhs = $this->expHelper($rhs, static::$precedence[$op] + 1); + + $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter]; + + $ss = $this->count; + $whiteBefore = isset($this->buffer[$this->count - 1]) && + ctype_space($this->buffer[$this->count - 1]); + } + + $this->seek($ss); + + return $lhs; + } + + /** + * Parse value + * + * @param array $out + * + * @return boolean + */ + protected function value(&$out) + { + if (! isset($this->buffer[$this->count])) { + return false; + } + + $s = $this->count; + $char = $this->buffer[$this->count]; + + if ( + $this->literal('url(', 4) && + $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false) + ) { + $len = strspn( + $this->buffer, + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=', + $this->count + ); + + $this->count += $len; + + if ($this->matchChar(')')) { + $content = substr($this->buffer, $s, $this->count - $s); + $out = [Type::T_KEYWORD, $content]; + + return true; + } + } + + $this->seek($s); + + if ( + $this->literal('url(', 4, false) && + $this->match('\s*(\/\/[^\s\)]+)\s*', $m) + ) { + $content = 'url(' . $m[1]; + + if ($this->matchChar(')')) { + $content .= ')'; + $out = [Type::T_KEYWORD, $content]; + + return true; + } + } + + $this->seek($s); + + // not + if ($char === 'n' && $this->literal('not', 3, false)) { + if ( + $this->whitespace() && + $this->value($inner) + ) { + $out = [Type::T_UNARY, 'not', $inner, $this->inParens]; + + return true; + } + + $this->seek($s); + + if ($this->parenValue($inner)) { + $out = [Type::T_UNARY, 'not', $inner, $this->inParens]; + + return true; + } + + $this->seek($s); + } + + // addition + if ($char === '+') { + $this->count++; + + $follow_white = $this->whitespace(); + + if ($this->value($inner)) { + $out = [Type::T_UNARY, '+', $inner, $this->inParens]; + + return true; + } + + if ($follow_white) { + $out = [Type::T_KEYWORD, $char]; + return true; + } + + $this->seek($s); + + return false; + } + + // negation + if ($char === '-') { + if ($this->customProperty($out)) { + return true; + } + + $this->count++; + + $follow_white = $this->whitespace(); + + if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) { + $out = [Type::T_UNARY, '-', $inner, $this->inParens]; + + return true; + } + + if ( + $this->keyword($inner) && + ! $this->func($inner, $out) + ) { + $out = [Type::T_UNARY, '-', $inner, $this->inParens]; + + return true; + } + + if ($follow_white) { + $out = [Type::T_KEYWORD, $char]; + + return true; + } + + $this->seek($s); + } + + // paren + if ($char === '(' && $this->parenValue($out)) { + return true; + } + + if ($char === '#') { + if ($this->interpolation($out) || $this->color($out)) { + return true; + } + + $this->count++; + + if ($this->keyword($keyword)) { + $out = [Type::T_KEYWORD, '#' . $keyword]; + + return true; + } + + $this->count--; + } + + if ($this->matchChar('&', true)) { + $out = [Type::T_SELF]; + + return true; + } + + if ($char === '$' && $this->variable($out)) { + return true; + } + + if ($char === 'p' && $this->progid($out)) { + return true; + } + + if (($char === '"' || $char === "'") && $this->string($out)) { + return true; + } + + if ($this->unit($out)) { + return true; + } + + // unicode range with wildcards + if ( + $this->literal('U+', 2) && + $this->match('\?+|([0-9A-F]+(\?+|(-[0-9A-F]+))?)', $m, false) + ) { + $unicode = explode('-', $m[0]); + if (strlen(reset($unicode)) <= 6 && strlen(end($unicode)) <= 6) { + $out = [Type::T_KEYWORD, 'U+' . $m[0]]; + + return true; + } + $this->count -= strlen($m[0]) + 2; + } + + if ($this->keyword($keyword, false)) { + if ($this->func($keyword, $out)) { + return true; + } + + $this->whitespace(); + + if ($keyword === 'null') { + $out = [Type::T_NULL]; + } else { + $out = [Type::T_KEYWORD, $keyword]; + } + + return true; + } + + return false; + } + + /** + * Parse parenthesized value + * + * @param array $out + * + * @return boolean + */ + protected function parenValue(&$out) + { + $s = $this->count; + + $inParens = $this->inParens; + + if ($this->matchChar('(')) { + if ($this->matchChar(')')) { + $out = [Type::T_LIST, '', []]; + + return true; + } + + $this->inParens = true; + + if ( + $this->expression($exp) && + $this->matchChar(')') + ) { + $out = $exp; + $this->inParens = $inParens; + + return true; + } + } + + $this->inParens = $inParens; + $this->seek($s); + + return false; + } + + /** + * Parse "progid:" + * + * @param array $out + * + * @return boolean + */ + protected function progid(&$out) + { + $s = $this->count; + + if ( + $this->literal('progid:', 7, false) && + $this->openString('(', $fn) && + $this->matchChar('(') + ) { + $this->openString(')', $args, '('); + + if ($this->matchChar(')')) { + $out = [Type::T_STRING, '', [ + 'progid:', $fn, '(', $args, ')' + ]]; + + return true; + } + } + + $this->seek($s); + + return false; + } + + /** + * Parse function call + * + * @param string $name + * @param array $func + * + * @return boolean + */ + protected function func($name, &$func) + { + $s = $this->count; + + if ($this->matchChar('(')) { + if ($name === 'alpha' && $this->argumentList($args)) { + $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]]; + + return true; + } + + if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) { + $ss = $this->count; + + if ( + $this->argValues($args) && + $this->matchChar(')') + ) { + $func = [Type::T_FUNCTION_CALL, $name, $args]; + + return true; + } + + $this->seek($ss); + } + + if ( + ($this->openString(')', $str, '(') || true) && + $this->matchChar(')') + ) { + $args = []; + + if (! empty($str)) { + $args[] = [null, [Type::T_STRING, '', [$str]]]; + } + + $func = [Type::T_FUNCTION_CALL, $name, $args]; + + return true; + } + } + + $this->seek($s); + + return false; + } + + /** + * Parse function call argument list + * + * @param array $out + * + * @return boolean + */ + protected function argumentList(&$out) + { + $s = $this->count; + $this->matchChar('('); + + $args = []; + + while ($this->keyword($var)) { + if ( + $this->matchChar('=') && + $this->expression($exp) + ) { + $args[] = [Type::T_STRING, '', [$var . '=']]; + $arg = $exp; + } else { + break; + } + + $args[] = $arg; + + if (! $this->matchChar(',')) { + break; + } + + $args[] = [Type::T_STRING, '', [', ']]; + } + + if (! $this->matchChar(')') || ! $args) { + $this->seek($s); + + return false; + } + + $out = $args; + + return true; + } + + /** + * Parse mixin/function definition argument list + * + * @param array $out + * + * @return boolean + */ + protected function argumentDef(&$out) + { + $s = $this->count; + $this->matchChar('('); + + $args = []; + + while ($this->variable($var)) { + $arg = [$var[1], null, false]; + + $ss = $this->count; + + if ( + $this->matchChar(':') && + $this->genericList($defaultVal, 'expression', '', true) + ) { + $arg[1] = $defaultVal; + } else { + $this->seek($ss); + } + + $ss = $this->count; + + if ($this->literal('...', 3)) { + $sss = $this->count; + + if (! $this->matchChar(')')) { + throw $this->parseError('... has to be after the final argument'); + } + + $arg[2] = true; + + $this->seek($sss); + } else { + $this->seek($ss); + } + + $args[] = $arg; + + if (! $this->matchChar(',')) { + break; + } + } + + if (! $this->matchChar(')')) { + $this->seek($s); + + return false; + } + + $out = $args; + + return true; + } + + /** + * Parse map + * + * @param array $out + * + * @return boolean + */ + protected function map(&$out) + { + $s = $this->count; + + if (! $this->matchChar('(')) { + return false; + } + + $keys = []; + $values = []; + + while ( + $this->genericList($key, 'expression', '', true) && + $this->matchChar(':') && + $this->genericList($value, 'expression', '', true) + ) { + $keys[] = $key; + $values[] = $value; + + if (! $this->matchChar(',')) { + break; + } + } + + if (! $keys || ! $this->matchChar(')')) { + $this->seek($s); + + return false; + } + + $out = [Type::T_MAP, $keys, $values]; + + return true; + } + + /** + * Parse color + * + * @param array $out + * + * @return boolean + */ + protected function color(&$out) + { + $s = $this->count; + + if ($this->match('(#([0-9a-f]+)\b)', $m)) { + if (\in_array(\strlen($m[2]), [3,4,6,8])) { + $out = [Type::T_KEYWORD, $m[0]]; + + return true; + } + + $this->seek($s); + + return false; + } + + return false; + } + + /** + * Parse number with unit + * + * @param array $unit + * + * @return boolean + */ + protected function unit(&$unit) + { + $s = $this->count; + + if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) { + if (\strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) { + $this->whitespace(); + + $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]); + + return true; + } + + $this->seek($s); + } + + return false; + } + + /** + * Parse string + * + * @param array $out + * + * @return boolean + */ + protected function string(&$out, $keepDelimWithInterpolation = false) + { + $s = $this->count; + + if ($this->matchChar('"', false)) { + $delim = '"'; + } elseif ($this->matchChar("'", false)) { + $delim = "'"; + } else { + return false; + } + + $content = []; + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + $hasInterpolation = false; + + while ($this->matchString($m, $delim)) { + if ($m[1] !== '') { + $content[] = $m[1]; + } + + if ($m[2] === '#{') { + $this->count -= \strlen($m[2]); + + if ($this->interpolation($inter, false)) { + $content[] = $inter; + $hasInterpolation = true; + } else { + $this->count += \strlen($m[2]); + $content[] = '#{'; // ignore it + } + } elseif ($m[2] === "\r") { + $content[] = chr(10); + // TODO : warning + # DEPRECATION WARNING on line x, column y of zzz: + # Unescaped multiline strings are deprecated and will be removed in a future version of Sass. + # To include a newline in a string, use "\a" or "\a " as in CSS. + if ($this->matchChar("\n", false)) { + $content[] = ' '; + } + } elseif ($m[2] === '\\') { + if ( + $this->literal("\r\n", 2, false) || + $this->matchChar("\r", false) || + $this->matchChar("\n", false) || + $this->matchChar("\f", false) + ) { + // this is a continuation escaping, to be ignored + } elseif ($this->matchEscapeCharacter($c)) { + $content[] = $c; + } else { + throw $this->parseError('Unterminated escape sequence'); + } + } else { + $this->count -= \strlen($delim); + break; // delim + } + } + + $this->eatWhiteDefault = $oldWhite; + + if ($this->literal($delim, \strlen($delim))) { + if ($hasInterpolation && ! $keepDelimWithInterpolation) { + $delim = '"'; + } + + $out = [Type::T_STRING, $delim, $content]; + + return true; + } + + $this->seek($s); + + return false; + } + + /** + * @param string $out + * @param bool $inKeywords + * @return bool + */ + protected function matchEscapeCharacter(&$out, $inKeywords = false) + { + $s = $this->count; + if ($this->match('[a-f0-9]', $m, false)) { + $hex = $m[0]; + + for ($i = 5; $i--;) { + if ($this->match('[a-f0-9]', $m, false)) { + $hex .= $m[0]; + } else { + break; + } + } + + // CSS allows Unicode escape sequences to be followed by a delimiter space + // (necessary in some cases for shorter sequences to disambiguate their end) + $this->matchChar(' ', false); + + $value = hexdec($hex); + + if (!$inKeywords && ($value == 0 || ($value >= 0xD800 && $value <= 0xDFFF) || $value >= 0x10FFFF)) { + $out = "\xEF\xBF\xBD"; // "\u{FFFD}" but with a syntax supported on PHP 5 + } elseif ($value < 0x20) { + $out = Util::mbChr($value); + } else { + $out = Util::mbChr($value); + } + + return true; + } + + if ($this->match('.', $m, false)) { + if ($inKeywords && in_array($m[0], ["'",'"','@','&',' ','\\',':','/','%'])) { + $this->seek($s); + return false; + } + $out = $m[0]; + + return true; + } + + return false; + } + + /** + * Parse keyword or interpolation + * + * @param array $out + * @param boolean $restricted + * + * @return boolean + */ + protected function mixedKeyword(&$out, $restricted = false) + { + $parts = []; + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + for (;;) { + if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) { + $parts[] = $key; + continue; + } + + if ($this->interpolation($inter)) { + $parts[] = $inter; + continue; + } + + break; + } + + $this->eatWhiteDefault = $oldWhite; + + if (! $parts) { + return false; + } + + if ($this->eatWhiteDefault) { + $this->whitespace(); + } + + $out = $parts; + + return true; + } + + /** + * Parse an unbounded string stopped by $end + * + * @param string $end + * @param array $out + * @param string $nestOpen + * @param string $nestClose + * @param boolean $rtrim + * @param string $disallow + * + * @return boolean + */ + protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null) + { + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + if ($nestOpen && ! $nestClose) { + $nestClose = $end; + } + + $patt = ($disallow ? '[^' . $this->pregQuote($disallow) . ']' : '.'); + $patt = '(' . $patt . '*?)([\'"]|#\{|' + . $this->pregQuote($end) . '|' + . (($nestClose && $nestClose !== $end) ? $this->pregQuote($nestClose) . '|' : '') + . static::$commentPattern . ')'; + + $nestingLevel = 0; + + $content = []; + + while ($this->match($patt, $m, false)) { + if (isset($m[1]) && $m[1] !== '') { + $content[] = $m[1]; + + if ($nestOpen) { + $nestingLevel += substr_count($m[1], $nestOpen); + } + } + + $tok = $m[2]; + + $this->count -= \strlen($tok); + + if ($tok === $end && ! $nestingLevel) { + break; + } + + if ($tok === $nestClose) { + $nestingLevel--; + } + + if (($tok === "'" || $tok === '"') && $this->string($str, true)) { + $content[] = $str; + continue; + } + + if ($tok === '#{' && $this->interpolation($inter)) { + $content[] = $inter; + continue; + } + + $content[] = $tok; + $this->count += \strlen($tok); + } + + $this->eatWhiteDefault = $oldWhite; + + if (! $content || $tok !== $end) { + return false; + } + + // trim the end + if ($rtrim && \is_string(end($content))) { + $content[\count($content) - 1] = rtrim(end($content)); + } + + $out = [Type::T_STRING, '', $content]; + + return true; + } + + /** + * Parser interpolation + * + * @param string|array $out + * @param boolean $lookWhite save information about whitespace before and after + * + * @return boolean + */ + protected function interpolation(&$out, $lookWhite = true) + { + $oldWhite = $this->eatWhiteDefault; + $allowVars = $this->allowVars; + $this->allowVars = true; + $this->eatWhiteDefault = true; + + $s = $this->count; + + if ( + $this->literal('#{', 2) && + $this->valueList($value) && + $this->matchChar('}', false) + ) { + if ($value === [Type::T_SELF]) { + $out = $value; + } else { + if ($lookWhite) { + $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : ''; + $right = ( + ! empty($this->buffer[$this->count]) && + preg_match('/\s/', $this->buffer[$this->count]) + ) ? ' ' : ''; + } else { + $left = $right = false; + } + + $out = [Type::T_INTERPOLATE, $value, $left, $right]; + } + + $this->eatWhiteDefault = $oldWhite; + $this->allowVars = $allowVars; + + if ($this->eatWhiteDefault) { + $this->whitespace(); + } + + return true; + } + + $this->seek($s); + + $this->eatWhiteDefault = $oldWhite; + $this->allowVars = $allowVars; + + return false; + } + + /** + * Parse property name (as an array of parts or a string) + * + * @param array $out + * + * @return boolean + */ + protected function propertyName(&$out) + { + $parts = []; + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + for (;;) { + if ($this->interpolation($inter)) { + $parts[] = $inter; + continue; + } + + if ($this->keyword($text)) { + $parts[] = $text; + continue; + } + + if (! $parts && $this->match('[:.#]', $m, false)) { + // css hacks + $parts[] = $m[0]; + continue; + } + + break; + } + + $this->eatWhiteDefault = $oldWhite; + + if (! $parts) { + return false; + } + + // match comment hack + if (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) { + if (! empty($m[0])) { + $parts[] = $m[0]; + $this->count += \strlen($m[0]); + } + } + + $this->whitespace(); // get any extra whitespace + + $out = [Type::T_STRING, '', $parts]; + + return true; + } + + /** + * Parse custom property name (as an array of parts or a string) + * + * @param array $out + * + * @return boolean + */ + protected function customProperty(&$out) + { + $s = $this->count; + + if (! $this->literal('--', 2, false)) { + return false; + } + + $parts = ['--']; + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + for (;;) { + if ($this->interpolation($inter)) { + $parts[] = $inter; + continue; + } + + if ($this->matchChar('&', false)) { + $parts[] = [Type::T_SELF]; + continue; + } + + if ($this->variable($var)) { + $parts[] = $var; + continue; + } + + if ($this->keyword($text)) { + $parts[] = $text; + continue; + } + + break; + } + + $this->eatWhiteDefault = $oldWhite; + + if (\count($parts) == 1) { + $this->seek($s); + + return false; + } + + $this->whitespace(); // get any extra whitespace + + $out = [Type::T_STRING, '', $parts]; + + return true; + } + + /** + * Parse comma separated selector list + * + * @param array $out + * @param string|boolean $subSelector + * + * @return boolean + */ + protected function selectors(&$out, $subSelector = false) + { + $s = $this->count; + $selectors = []; + + while ($this->selector($sel, $subSelector)) { + $selectors[] = $sel; + + if (! $this->matchChar(',', true)) { + break; + } + + while ($this->matchChar(',', true)) { + ; // ignore extra + } + } + + if (! $selectors) { + $this->seek($s); + + return false; + } + + $out = $selectors; + + return true; + } + + /** + * Parse whitespace separated selector list + * + * @param array $out + * @param string|boolean $subSelector + * + * @return boolean + */ + protected function selector(&$out, $subSelector = false) + { + $selector = []; + + $discardComments = $this->discardComments; + $this->discardComments = true; + + for (;;) { + $s = $this->count; + + if ($this->match('[>+~]+', $m, true)) { + if ( + $subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 && + $m[0] === '+' && $this->match("(\d+|n\b)", $counter) + ) { + $this->seek($s); + } else { + $selector[] = [$m[0]]; + continue; + } + } + + if ($this->selectorSingle($part, $subSelector)) { + $selector[] = $part; + $this->whitespace(); + continue; + } + + break; + } + + $this->discardComments = $discardComments; + + if (! $selector) { + return false; + } + + $out = $selector; + + return true; + } + + /** + * parsing escaped chars in selectors: + * - escaped single chars are kept escaped in the selector but in a normalized form + * (if not in 0-9a-f range as this would be ambigous) + * - other escaped sequences (multibyte chars or 0-9a-f) are kept in their initial escaped form, + * normalized to lowercase + * + * TODO: this is a fallback solution. Ideally escaped chars in selectors should be encoded as the genuine chars, + * and escaping added when printing in the Compiler, where/if it's mandatory + * - but this require a better formal selector representation instead of the array we have now + * + * @param string $out + * @param bool $keepEscapedNumber + * @return bool + */ + protected function matchEscapeCharacterInSelector(&$out, $keepEscapedNumber = false) + { + $s_escape = $this->count; + if ($this->match('\\\\', $m)) { + $out = '\\' . $m[0]; + return true; + } + + if ($this->matchEscapeCharacter($escapedout, true)) { + if (strlen($escapedout) === 1) { + if (!preg_match(",\w,", $escapedout)) { + $out = '\\' . $escapedout; + return true; + } elseif (! $keepEscapedNumber || ! \is_numeric($escapedout)) { + $out = $escapedout; + return true; + } + } + $escape_sequence = rtrim(substr($this->buffer, $s_escape, $this->count - $s_escape)); + if (strlen($escape_sequence) < 6) { + $escape_sequence .= ' '; + } + $out = '\\' . strtolower($escape_sequence); + return true; + } + if ($this->match('\\S', $m)) { + $out = '\\' . $m[0]; + return true; + } + + + return false; + } + + /** + * Parse the parts that make up a selector + * + * {@internal + * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder + * }} + * + * @param array $out + * @param string|boolean $subSelector + * + * @return boolean + */ + protected function selectorSingle(&$out, $subSelector = false) + { + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + $parts = []; + + if ($this->matchChar('*', false)) { + $parts[] = '*'; + } + + for (;;) { + if (! isset($this->buffer[$this->count])) { + break; + } + + $s = $this->count; + $char = $this->buffer[$this->count]; + + // see if we can stop early + if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') { + break; + } + + // parsing a sub selector in () stop with the closing ) + if ($subSelector && $char === ')') { + break; + } + + //self + switch ($char) { + case '&': + $parts[] = Compiler::$selfSelector; + $this->count++; + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + continue 2; + + case '.': + $parts[] = '.'; + $this->count++; + continue 2; + + case '|': + $parts[] = '|'; + $this->count++; + continue 2; + } + + // handling of escaping in selectors : get the escaped char + if ($char === '\\') { + $this->count++; + if ($this->matchEscapeCharacterInSelector($escaped, true)) { + $parts[] = $escaped; + continue; + } + $this->count--; + } + + if ($char === '%') { + $this->count++; + + if ($this->placeholder($placeholder)) { + $parts[] = '%'; + $parts[] = $placeholder; + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + continue; + } + + break; + } + + if ($char === '#') { + if ($this->interpolation($inter)) { + $parts[] = $inter; + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + continue; + } + + $parts[] = '#'; + $this->count++; + continue; + } + + // a pseudo selector + if ($char === ':') { + if ($this->buffer[$this->count + 1] === ':') { + $this->count += 2; + $part = '::'; + } else { + $this->count++; + $part = ':'; + } + + if ($this->mixedKeyword($nameParts, true)) { + $parts[] = $part; + + foreach ($nameParts as $sub) { + $parts[] = $sub; + } + + $ss = $this->count; + + if ( + $nameParts === ['not'] || + $nameParts === ['is'] || + $nameParts === ['has'] || + $nameParts === ['where'] || + $nameParts === ['slotted'] || + $nameParts === ['nth-child'] || + $nameParts === ['nth-last-child'] || + $nameParts === ['nth-of-type'] || + $nameParts === ['nth-last-of-type'] + ) { + if ( + $this->matchChar('(', true) && + ($this->selectors($subs, reset($nameParts)) || true) && + $this->matchChar(')') + ) { + $parts[] = '('; + + while ($sub = array_shift($subs)) { + while ($ps = array_shift($sub)) { + foreach ($ps as &$p) { + $parts[] = $p; + } + + if (\count($sub) && reset($sub)) { + $parts[] = ' '; + } + } + + if (\count($subs) && reset($subs)) { + $parts[] = ', '; + } + } + + $parts[] = ')'; + } else { + $this->seek($ss); + } + } elseif ( + $this->matchChar('(', true) && + ($this->openString(')', $str, '(') || true) && + $this->matchChar(')') + ) { + $parts[] = '('; + + if (! empty($str)) { + $parts[] = $str; + } + + $parts[] = ')'; + } else { + $this->seek($ss); + } + + continue; + } + } + + $this->seek($s); + + // 2n+1 + if ($subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0) { + if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) { + $parts[] = $counter[0]; + //$parts[] = str_replace(' ', '', $counter[0]); + continue; + } + } + + $this->seek($s); + + // attribute selector + if ( + $char === '[' && + $this->matchChar('[') && + ($this->openString(']', $str, '[') || true) && + $this->matchChar(']') + ) { + $parts[] = '['; + + if (! empty($str)) { + $parts[] = $str; + } + + $parts[] = ']'; + continue; + } + + $this->seek($s); + + // for keyframes + if ($this->unit($unit)) { + $parts[] = $unit; + continue; + } + + if ($this->restrictedKeyword($name, false, true)) { + $parts[] = $name; + continue; + } + + break; + } + + $this->eatWhiteDefault = $oldWhite; + + if (! $parts) { + return false; + } + + $out = $parts; + + return true; + } + + /** + * Parse a variable + * + * @param array $out + * + * @return boolean + */ + protected function variable(&$out) + { + $s = $this->count; + + if ( + $this->matchChar('$', false) && + $this->keyword($name) + ) { + if ($this->allowVars) { + $out = [Type::T_VARIABLE, $name]; + } else { + $out = [Type::T_KEYWORD, '$' . $name]; + } + + return true; + } + + $this->seek($s); + + return false; + } + + /** + * Parse a keyword + * + * @param string $word + * @param boolean $eatWhitespace + * @param boolean $inSelector + * + * @return boolean + */ + protected function keyword(&$word, $eatWhitespace = null, $inSelector = false) + { + $s = $this->count; + $match = $this->match( + $this->utf8 + ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)' + : '(([\w_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\w\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)', + $m, + false + ); + + if ($match) { + $word = $m[1]; + + // handling of escaping in keyword : get the escaped char + if (strpos($word, '\\') !== false) { + $send = $this->count; + $escapedWord = []; + $this->seek($s); + $previousEscape = false; + while ($this->count < $send) { + $char = $this->buffer[$this->count]; + $this->count++; + if ( + $this->count < $send + && $char === '\\' + && !$previousEscape + && ( + $inSelector ? + $this->matchEscapeCharacterInSelector($out) + : + $this->matchEscapeCharacter($out, true) + ) + ) { + $escapedWord[] = $out; + } else { + if ($previousEscape) { + $previousEscape = false; + } elseif ($char === '\\') { + $previousEscape = true; + } + $escapedWord[] = $char; + } + } + + $word = implode('', $escapedWord); + } + + if (is_null($eatWhitespace) ? $this->eatWhiteDefault : $eatWhitespace) { + $this->whitespace(); + } + + return true; + } + + return false; + } + + /** + * Parse a keyword that should not start with a number + * + * @param string $word + * @param boolean $eatWhitespace + * @param boolean $inSelector + * + * @return boolean + */ + protected function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false) + { + $s = $this->count; + + if ($this->keyword($word, $eatWhitespace, $inSelector) && (\ord($word[0]) > 57 || \ord($word[0]) < 48)) { + return true; + } + + $this->seek($s); + + return false; + } + + /** + * Parse a placeholder + * + * @param string|array $placeholder + * + * @return boolean + */ + protected function placeholder(&$placeholder) + { + $match = $this->match( + $this->utf8 + ? '([\pL\w\-_]+)' + : '([\w\-_]+)', + $m + ); + + if ($match) { + $placeholder = $m[1]; + + return true; + } + + if ($this->interpolation($placeholder)) { + return true; + } + + return false; + } + + /** + * Parse a url + * + * @param array $out + * + * @return boolean + */ + protected function url(&$out) + { + if ($this->literal('url(', 4)) { + $s = $this->count; + + if ( + ($this->string($out) || $this->spaceList($out)) && + $this->matchChar(')') + ) { + $out = [Type::T_STRING, '', ['url(', $out, ')']]; + + return true; + } + + $this->seek($s); + + if ( + $this->openString(')', $out) && + $this->matchChar(')') + ) { + $out = [Type::T_STRING, '', ['url(', $out, ')']]; + + return true; + } + } + + return false; + } + + /** + * Consume an end of statement delimiter + * @param bool $eatWhitespace + * + * @return boolean + */ + protected function end($eatWhitespace = null) + { + if ($this->matchChar(';', $eatWhitespace)) { + return true; + } + + if ($this->count === \strlen($this->buffer) || $this->buffer[$this->count] === '}') { + // if there is end of file or a closing block next then we don't need a ; + return true; + } + + return false; + } + + /** + * Strip assignment flag from the list + * + * @param array $value + * + * @return array + */ + protected function stripAssignmentFlags(&$value) + { + $flags = []; + + for ($token = &$value; $token[0] === Type::T_LIST && ($s = \count($token[2])); $token = &$lastNode) { + $lastNode = &$token[2][$s - 1]; + + while ($lastNode[0] === Type::T_KEYWORD && \in_array($lastNode[1], ['!default', '!global'])) { + array_pop($token[2]); + + $node = end($token[2]); + $token = $this->flattenList($token); + $flags[] = $lastNode[1]; + $lastNode = $node; + } + } + + return $flags; + } + + /** + * Strip optional flag from selector list + * + * @param array $selectors + * + * @return string + */ + protected function stripOptionalFlag(&$selectors) + { + $optional = false; + $selector = end($selectors); + $part = end($selector); + + if ($part === ['!optional']) { + array_pop($selectors[\count($selectors) - 1]); + + $optional = true; + } + + return $optional; + } + + /** + * Turn list of length 1 into value type + * + * @param array $value + * + * @return array + */ + protected function flattenList($value) + { + if ($value[0] === Type::T_LIST && \count($value[2]) === 1) { + return $this->flattenList($value[2][0]); + } + + return $value; + } + + /** + * Quote regular expression + * + * @param string $what + * + * @return string + */ + private function pregQuote($what) + { + return preg_quote($what, '/'); + } + + /** + * Extract line numbers from buffer + * + * @param string $buffer + */ + private function extractLineNumbers($buffer) + { + $this->sourcePositions = [0 => 0]; + $prev = 0; + + while (($pos = strpos($buffer, "\n", $prev)) !== false) { + $this->sourcePositions[] = $pos; + $prev = $pos + 1; + } + + $this->sourcePositions[] = \strlen($buffer); + + if (substr($buffer, -1) !== "\n") { + $this->sourcePositions[] = \strlen($buffer) + 1; + } + } + + /** + * Get source line number and column (given character position in the buffer) + * + * @param integer $pos + * + * @return array + */ + private function getSourcePosition($pos) + { + $low = 0; + $high = \count($this->sourcePositions); + + while ($low < $high) { + $mid = (int) (($high + $low) / 2); + + if ($pos < $this->sourcePositions[$mid]) { + $high = $mid - 1; + continue; + } + + if ($pos >= $this->sourcePositions[$mid + 1]) { + $low = $mid + 1; + continue; + } + + return [$mid + 1, $pos - $this->sourcePositions[$mid]]; + } + + return [$low + 1, $pos - $this->sourcePositions[$low]]; + } + + /** + * Save internal encoding of mbstring + * + * When mbstring.func_overload is used to replace the standard PHP string functions, + * this method configures the internal encoding to a single-byte one so that the + * behavior matches the normal behavior of PHP string functions while using the parser. + * The existing internal encoding is saved and will be restored when calling {@see restoreEncoding}. + * + * If mbstring.func_overload is not used (or does not override string functions), this method is a no-op. + * + * @return void + */ + private function saveEncoding() + { + if (\PHP_VERSION_ID < 80000 && \extension_loaded('mbstring') && (2 & (int) ini_get('mbstring.func_overload')) > 0) { + $this->encoding = mb_internal_encoding(); + + mb_internal_encoding('iso-8859-1'); + } + } + + /** + * Restore internal encoding + * + * @return void + */ + private function restoreEncoding() + { + if (\extension_loaded('mbstring') && $this->encoding) { + mb_internal_encoding($this->encoding); + } + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64.php b/user/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64.php new file mode 100644 index 0000000..4a5ed8b --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64.php @@ -0,0 +1,187 @@ + + * + * @internal + */ +class Base64 +{ + /** + * @var array + */ + private static $encodingMap = [ + 0 => 'A', + 1 => 'B', + 2 => 'C', + 3 => 'D', + 4 => 'E', + 5 => 'F', + 6 => 'G', + 7 => 'H', + 8 => 'I', + 9 => 'J', + 10 => 'K', + 11 => 'L', + 12 => 'M', + 13 => 'N', + 14 => 'O', + 15 => 'P', + 16 => 'Q', + 17 => 'R', + 18 => 'S', + 19 => 'T', + 20 => 'U', + 21 => 'V', + 22 => 'W', + 23 => 'X', + 24 => 'Y', + 25 => 'Z', + 26 => 'a', + 27 => 'b', + 28 => 'c', + 29 => 'd', + 30 => 'e', + 31 => 'f', + 32 => 'g', + 33 => 'h', + 34 => 'i', + 35 => 'j', + 36 => 'k', + 37 => 'l', + 38 => 'm', + 39 => 'n', + 40 => 'o', + 41 => 'p', + 42 => 'q', + 43 => 'r', + 44 => 's', + 45 => 't', + 46 => 'u', + 47 => 'v', + 48 => 'w', + 49 => 'x', + 50 => 'y', + 51 => 'z', + 52 => '0', + 53 => '1', + 54 => '2', + 55 => '3', + 56 => '4', + 57 => '5', + 58 => '6', + 59 => '7', + 60 => '8', + 61 => '9', + 62 => '+', + 63 => '/', + ]; + + /** + * @var array + */ + private static $decodingMap = [ + 'A' => 0, + 'B' => 1, + 'C' => 2, + 'D' => 3, + 'E' => 4, + 'F' => 5, + 'G' => 6, + 'H' => 7, + 'I' => 8, + 'J' => 9, + 'K' => 10, + 'L' => 11, + 'M' => 12, + 'N' => 13, + 'O' => 14, + 'P' => 15, + 'Q' => 16, + 'R' => 17, + 'S' => 18, + 'T' => 19, + 'U' => 20, + 'V' => 21, + 'W' => 22, + 'X' => 23, + 'Y' => 24, + 'Z' => 25, + 'a' => 26, + 'b' => 27, + 'c' => 28, + 'd' => 29, + 'e' => 30, + 'f' => 31, + 'g' => 32, + 'h' => 33, + 'i' => 34, + 'j' => 35, + 'k' => 36, + 'l' => 37, + 'm' => 38, + 'n' => 39, + 'o' => 40, + 'p' => 41, + 'q' => 42, + 'r' => 43, + 's' => 44, + 't' => 45, + 'u' => 46, + 'v' => 47, + 'w' => 48, + 'x' => 49, + 'y' => 50, + 'z' => 51, + 0 => 52, + 1 => 53, + 2 => 54, + 3 => 55, + 4 => 56, + 5 => 57, + 6 => 58, + 7 => 59, + 8 => 60, + 9 => 61, + '+' => 62, + '/' => 63, + ]; + + /** + * Convert to base64 + * + * @param integer $value + * + * @return string + */ + public static function encode($value) + { + return self::$encodingMap[$value]; + } + + /** + * Convert from base64 + * + * @param string $value + * + * @return integer + */ + public static function decode($value) + { + return self::$decodingMap[$value]; + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64VLQ.php b/user/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64VLQ.php new file mode 100644 index 0000000..d47b96a --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/Base64VLQ.php @@ -0,0 +1,151 @@ + + * @author Anthon Pang + * + * @internal + */ +class Base64VLQ +{ + // A Base64 VLQ digit can represent 5 bits, so it is base-32. + const VLQ_BASE_SHIFT = 5; + + // A mask of bits for a VLQ digit (11111), 31 decimal. + const VLQ_BASE_MASK = 31; + + // The continuation bit is the 6th bit. + const VLQ_CONTINUATION_BIT = 32; + + /** + * Returns the VLQ encoded value. + * + * @param integer $value + * + * @return string + */ + public static function encode($value) + { + $encoded = ''; + $vlq = self::toVLQSigned($value); + + do { + $digit = $vlq & self::VLQ_BASE_MASK; + + //$vlq >>>= self::VLQ_BASE_SHIFT; // unsigned right shift + $vlq = (($vlq >> 1) & PHP_INT_MAX) >> (self::VLQ_BASE_SHIFT - 1); + + if ($vlq > 0) { + $digit |= self::VLQ_CONTINUATION_BIT; + } + + $encoded .= Base64::encode($digit); + } while ($vlq > 0); + + return $encoded; + } + + /** + * Decodes VLQValue. + * + * @param string $str + * @param integer $index + * + * @return integer + */ + public static function decode($str, &$index) + { + $result = 0; + $shift = 0; + + do { + $c = $str[$index++]; + $digit = Base64::decode($c); + $continuation = ($digit & self::VLQ_CONTINUATION_BIT) != 0; + $digit &= self::VLQ_BASE_MASK; + $result = $result + ($digit << $shift); + $shift = $shift + self::VLQ_BASE_SHIFT; + } while ($continuation); + + return self::fromVLQSigned($result); + } + + /** + * Converts from a two-complement value to a value where the sign bit is + * is placed in the least significant bit. For example, as decimals: + * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary) + * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary) + * + * @param integer $value + * + * @return integer + */ + private static function toVLQSigned($value) + { + if ($value < 0) { + return ((-$value) << 1) + 1; + } + + return ($value << 1) + 0; + } + + /** + * Converts to a two-complement value from a value where the sign bit is + * is placed in the least significant bit. For example, as decimals: + * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1 + * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2 + * + * @param integer $value + * + * @return integer + */ + private static function fromVLQSigned($value) + { + $negate = ($value & 1) === 1; + + //$value >>>= 1; // unsigned right shift + $value = ($value >> 1) & PHP_INT_MAX; + + if (! $negate) { + return $value; + } + + // We need to OR 0x80000000 here to ensure the 32nd bit (the sign bit) is + // always set for negative numbers. If `value` were 1, (meaning `negate` is + // true and all other bits were zeros), `value` would now be 0. -0 is just + // 0, and doesn't flip the 32nd bit as intended. All positive numbers will + // successfully flip the 32nd bit without issue, so it's a noop for them. + return -$value | 0x80000000; + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php b/user/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php new file mode 100644 index 0000000..4f14bdc --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php @@ -0,0 +1,381 @@ + + * @author Nicolas FRANÇOIS + * + * @internal + */ +class SourceMapGenerator +{ + /** + * What version of source map does the generator generate? + */ + const VERSION = 3; + + /** + * Array of default options + * + * @var array + * @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string} + */ + protected $defaultOptions = [ + // an optional source root, useful for relocating source files + // on a server or removing repeated values in the 'sources' entry. + // This value is prepended to the individual entries in the 'source' field. + 'sourceRoot' => '', + + // an optional name of the generated code that this source map is associated with. + 'sourceMapFilename' => null, + + // url of the map + 'sourceMapURL' => null, + + // absolute path to a file to write the map to + 'sourceMapWriteTo' => null, + + // output source contents? + 'outputSourceFiles' => false, + + // base path for filename normalization + 'sourceMapRootpath' => '', + + // base path for filename normalization + 'sourceMapBasepath' => '' + ]; + + /** + * The base64 VLQ encoder + * + * @var \ScssPhp\ScssPhp\SourceMap\Base64VLQ + */ + protected $encoder; + + /** + * Array of mappings + * + * @var array + * @phpstan-var list + */ + protected $mappings = []; + + /** + * Array of contents map + * + * @var array + */ + protected $contentsMap = []; + + /** + * File to content map + * + * @var array + */ + protected $sources = []; + + /** + * @var array + */ + protected $sourceKeys = []; + + /** + * @var array + * @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string} + */ + private $options; + + /** + * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $options + */ + public function __construct(array $options = []) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->encoder = new Base64VLQ(); + } + + /** + * Adds a mapping + * + * @param integer $generatedLine The line number in generated file + * @param integer $generatedColumn The column number in generated file + * @param integer $originalLine The line number in original file + * @param integer $originalColumn The column number in original file + * @param string $sourceFile The original source file + * + * @return void + */ + public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile) + { + $this->mappings[] = [ + 'generated_line' => $generatedLine, + 'generated_column' => $generatedColumn, + 'original_line' => $originalLine, + 'original_column' => $originalColumn, + 'source_file' => $sourceFile + ]; + + $this->sources[$sourceFile] = $sourceFile; + } + + /** + * Saves the source map to a file + * + * @param string $content The content to write + * + * @return string + * + * @throws \ScssPhp\ScssPhp\Exception\CompilerException If the file could not be saved + * @deprecated + */ + public function saveMap($content) + { + $file = $this->options['sourceMapWriteTo']; + $dir = \dirname($file); + + // directory does not exist + if (! is_dir($dir)) { + // FIXME: create the dir automatically? + throw new CompilerException( + sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir) + ); + } + + // FIXME: proper saving, with dir write check! + if (file_put_contents($file, $content) === false) { + throw new CompilerException(sprintf('Cannot save the source map to "%s"', $file)); + } + + return $this->options['sourceMapURL']; + } + + /** + * Generates the JSON source map + * + * @param string $prefix A prefix added in the output file, which needs to shift mappings + * + * @return string + * + * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit# + */ + public function generateJson($prefix = '') + { + $sourceMap = []; + $mappings = $this->generateMappings($prefix); + + // File version (always the first entry in the object) and must be a positive integer. + $sourceMap['version'] = self::VERSION; + + // An optional name of the generated code that this source map is associated with. + $file = $this->options['sourceMapFilename']; + + if ($file) { + $sourceMap['file'] = $file; + } + + // An optional source root, useful for relocating source files on a server or removing repeated values in the + // 'sources' entry. This value is prepended to the individual entries in the 'source' field. + $root = $this->options['sourceRoot']; + + if ($root) { + $sourceMap['sourceRoot'] = $root; + } + + // A list of original sources used by the 'mappings' entry. + $sourceMap['sources'] = []; + + foreach ($this->sources as $sourceUri => $sourceFilename) { + $sourceMap['sources'][] = $this->normalizeFilename($sourceFilename); + } + + // A list of symbol names used by the 'mappings' entry. + $sourceMap['names'] = []; + + // A string with the encoded mapping data. + $sourceMap['mappings'] = $mappings; + + if ($this->options['outputSourceFiles']) { + // An optional list of source content, useful when the 'source' can't be hosted. + // The contents are listed in the same order as the sources above. + // 'null' may be used if some original sources should be retrieved by name. + $sourceMap['sourcesContent'] = $this->getSourcesContent(); + } + + // less.js compat fixes + if (\count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) { + unset($sourceMap['sourceRoot']); + } + + return json_encode($sourceMap, JSON_UNESCAPED_SLASHES); + } + + /** + * Returns the sources contents + * + * @return string[]|null + */ + protected function getSourcesContent() + { + if (empty($this->sources)) { + return null; + } + + $content = []; + + foreach ($this->sources as $sourceFile) { + $content[] = file_get_contents($sourceFile); + } + + return $content; + } + + /** + * Generates the mappings string + * + * @param string $prefix A prefix added in the output file, which needs to shift mappings + * + * @return string + */ + public function generateMappings($prefix = '') + { + if (! \count($this->mappings)) { + return ''; + } + + $prefixLines = substr_count($prefix, "\n"); + $lastPrefixNewLine = strrpos($prefix, "\n"); + $lastPrefixLineStart = false === $lastPrefixNewLine ? 0 : $lastPrefixNewLine + 1; + $prefixColumn = strlen($prefix) - $lastPrefixLineStart; + + $this->sourceKeys = array_flip(array_keys($this->sources)); + + // group mappings by generated line number. + $groupedMap = $groupedMapEncoded = []; + + foreach ($this->mappings as $m) { + $groupedMap[$m['generated_line']][] = $m; + } + + ksort($groupedMap); + + $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0; + + foreach ($groupedMap as $lineNumber => $lineMap) { + if ($lineNumber > 1) { + // The prefix only impacts the column for the first line of the original output + $prefixColumn = 0; + } + $lineNumber += $prefixLines; + + while (++$lastGeneratedLine < $lineNumber) { + $groupedMapEncoded[] = ';'; + } + + $lineMapEncoded = []; + $lastGeneratedColumn = 0; + + foreach ($lineMap as $m) { + $generatedColumn = $m['generated_column'] + $prefixColumn; + + $mapEncoded = $this->encoder->encode($generatedColumn - $lastGeneratedColumn); + $lastGeneratedColumn = $generatedColumn; + + // find the index + if ($m['source_file']) { + $index = $this->findFileIndex($m['source_file']); + + if ($index !== false) { + $mapEncoded .= $this->encoder->encode($index - $lastOriginalIndex); + $lastOriginalIndex = $index; + // lines are stored 0-based in SourceMap spec version 3 + $mapEncoded .= $this->encoder->encode($m['original_line'] - 1 - $lastOriginalLine); + $lastOriginalLine = $m['original_line'] - 1; + $mapEncoded .= $this->encoder->encode($m['original_column'] - $lastOriginalColumn); + $lastOriginalColumn = $m['original_column']; + } + } + + $lineMapEncoded[] = $mapEncoded; + } + + $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';'; + } + + return rtrim(implode($groupedMapEncoded), ';'); + } + + /** + * Finds the index for the filename + * + * @param string $filename + * + * @return integer|false + */ + protected function findFileIndex($filename) + { + return $this->sourceKeys[$filename]; + } + + /** + * Normalize filename + * + * @param string $filename + * + * @return string + */ + protected function normalizeFilename($filename) + { + $filename = $this->fixWindowsPath($filename); + $rootpath = $this->options['sourceMapRootpath']; + $basePath = $this->options['sourceMapBasepath']; + + // "Trim" the 'sourceMapBasepath' from the output filename. + if (\strlen($basePath) && strpos($filename, $basePath) === 0) { + $filename = substr($filename, \strlen($basePath)); + } + + // Remove extra leading path separators. + if (strpos($filename, '\\') === 0 || strpos($filename, '/') === 0) { + $filename = substr($filename, 1); + } + + return $rootpath . $filename; + } + + /** + * Fix windows paths + * + * @param string $path + * @param boolean $addEndSlash + * + * @return string + */ + public function fixWindowsPath($path, $addEndSlash = false) + { + $slash = ($addEndSlash) ? '/' : ''; + + if (! empty($path)) { + $path = str_replace('\\', '/', $path); + $path = rtrim($path, '/') . $slash; + } + + return $path; + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Type.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Type.php new file mode 100644 index 0000000..fb2a1d7 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Type.php @@ -0,0 +1,76 @@ + + */ +class Type +{ + const T_ASSIGN = 'assign'; + const T_AT_ROOT = 'at-root'; + const T_BLOCK = 'block'; + /** @deprecated */ + const T_BREAK = 'break'; + const T_CHARSET = 'charset'; + const T_COLOR = 'color'; + const T_COMMENT = 'comment'; + /** @deprecated */ + const T_CONTINUE = 'continue'; + /** @deprecated */ + const T_CONTROL = 'control'; + const T_CUSTOM_PROPERTY = 'custom'; + const T_DEBUG = 'debug'; + const T_DIRECTIVE = 'directive'; + const T_EACH = 'each'; + const T_ELSE = 'else'; + const T_ELSEIF = 'elseif'; + const T_ERROR = 'error'; + const T_EXPRESSION = 'exp'; + const T_EXTEND = 'extend'; + const T_FOR = 'for'; + const T_FUNCTION = 'function'; + const T_FUNCTION_REFERENCE = 'function-reference'; + const T_FUNCTION_CALL = 'fncall'; + const T_HSL = 'hsl'; + const T_HWB = 'hwb'; + const T_IF = 'if'; + const T_IMPORT = 'import'; + const T_INCLUDE = 'include'; + const T_INTERPOLATE = 'interpolate'; + const T_INTERPOLATED = 'interpolated'; + const T_KEYWORD = 'keyword'; + const T_LIST = 'list'; + const T_MAP = 'map'; + const T_MEDIA = 'media'; + const T_MEDIA_EXPRESSION = 'mediaExp'; + const T_MEDIA_TYPE = 'mediaType'; + const T_MEDIA_VALUE = 'mediaValue'; + const T_MIXIN = 'mixin'; + const T_MIXIN_CONTENT = 'mixin_content'; + const T_NESTED_PROPERTY = 'nestedprop'; + const T_NOT = 'not'; + const T_NULL = 'null'; + const T_NUMBER = 'number'; + const T_RETURN = 'return'; + const T_ROOT = 'root'; + const T_SCSSPHP_IMPORT_ONCE = 'scssphp-import-once'; + const T_SELF = 'self'; + const T_STRING = 'string'; + const T_UNARY = 'unary'; + const T_VARIABLE = 'var'; + const T_WARN = 'warn'; + const T_WHILE = 'while'; +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Util.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Util.php new file mode 100644 index 0000000..62cd2a2 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Util.php @@ -0,0 +1,184 @@ + + * + * @internal + */ +class Util +{ + /** + * Asserts that `value` falls within `range` (inclusive), leaving + * room for slight floating-point errors. + * + * @param string $name The name of the value. Used in the error message. + * @param Range $range Range of values. + * @param array|Number $value The value to check. + * @param string $unit The unit of the value. Used in error reporting. + * + * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin. + * + * @throws \ScssPhp\ScssPhp\Exception\RangeException + */ + public static function checkRange($name, Range $range, $value, $unit = '') + { + $val = $value[1]; + $grace = new Range(-0.00001, 0.00001); + + if (! \is_numeric($val)) { + throw new RangeException("$name {$val} is not a number."); + } + + if ($range->includes($val)) { + return $val; + } + + if ($grace->includes($val - $range->first)) { + return $range->first; + } + + if ($grace->includes($val - $range->last)) { + return $range->last; + } + + throw new RangeException("$name {$val} must be between {$range->first} and {$range->last}$unit"); + } + + /** + * Encode URI component + * + * @param string $string + * + * @return string + */ + public static function encodeURIComponent($string) + { + $revert = ['%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')']; + + return strtr(rawurlencode($string), $revert); + } + + /** + * mb_chr() wrapper + * + * @param integer $code + * + * @return string + */ + public static function mbChr($code) + { + // Use the native implementation if available, but not on PHP 7.2 as mb_chr(0) is buggy there + if (\PHP_VERSION_ID > 70300 && \function_exists('mb_chr')) { + return mb_chr($code, 'UTF-8'); + } + + if (0x80 > $code %= 0x200000) { + $s = \chr($code); + } elseif (0x800 > $code) { + $s = \chr(0xC0 | $code >> 6) . \chr(0x80 | $code & 0x3F); + } elseif (0x10000 > $code) { + $s = \chr(0xE0 | $code >> 12) . \chr(0x80 | $code >> 6 & 0x3F) . \chr(0x80 | $code & 0x3F); + } else { + $s = \chr(0xF0 | $code >> 18) . \chr(0x80 | $code >> 12 & 0x3F) + . \chr(0x80 | $code >> 6 & 0x3F) . \chr(0x80 | $code & 0x3F); + } + + return $s; + } + + /** + * mb_strlen() wrapper + * + * @param string $string + * @return int + */ + public static function mbStrlen($string) + { + // Use the native implementation if available. + if (\function_exists('mb_strlen')) { + return mb_strlen($string, 'UTF-8'); + } + + if (\function_exists('iconv_strlen')) { + return (int) @iconv_strlen($string, 'UTF-8'); + } + + throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.'); + } + + /** + * mb_substr() wrapper + * @param string $string + * @param int $start + * @param null|int $length + * @return string + */ + public static function mbSubstr($string, $start, $length = null) + { + // Use the native implementation if available. + if (\function_exists('mb_substr')) { + return mb_substr($string, $start, $length, 'UTF-8'); + } + + if (\function_exists('iconv_substr')) { + if ($start < 0) { + $start = static::mbStrlen($string) + $start; + if ($start < 0) { + $start = 0; + } + } + + if (null === $length) { + $length = 2147483647; + } elseif ($length < 0) { + $length = static::mbStrlen($string) + $length - $start; + if ($length < 0) { + return ''; + } + } + + return (string)iconv_substr($string, $start, $length, 'UTF-8'); + } + + throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.'); + } + + /** + * mb_strpos wrapper + * @param string $haystack + * @param string $needle + * @param int $offset + * + * @return int|false + */ + public static function mbStrpos($haystack, $needle, $offset = 0) + { + if (\function_exists('mb_strpos')) { + return mb_strpos($haystack, $needle, $offset, 'UTF-8'); + } + + if (\function_exists('iconv_strpos')) { + return iconv_strpos($haystack, $needle, $offset, 'UTF-8'); + } + + throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.'); + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Util/Path.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Util/Path.php new file mode 100644 index 0000000..f399e41 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Util/Path.php @@ -0,0 +1,77 @@ +parseValue($source, $value)) { + throw new \InvalidArgumentException(sprintf('Invalid value source "%s".', $source)); + } + + return $value; + } + + /** + * Converts a PHP value to a Sass value + * + * The returned value is guaranteed to be supported by the + * Compiler methods for registering custom variables. No other + * guarantee about it is provided. It should be considered + * opaque values by the caller. + * + * @param mixed $value + * + * @return mixed + */ + public static function fromPhp($value) + { + if ($value instanceof Number) { + return $value; + } + + if (is_array($value) && isset($value[0]) && \in_array($value[0], [Type::T_NULL, Type::T_COLOR, Type::T_KEYWORD, Type::T_LIST, Type::T_MAP, Type::T_STRING])) { + return $value; + } + + if ($value === null) { + return Compiler::$null; + } + + if ($value === true) { + return Compiler::$true; + } + + if ($value === false) { + return Compiler::$false; + } + + if ($value === '') { + return Compiler::$emptyString; + } + + if (\is_int($value) || \is_float($value)) { + return new Number($value, ''); + } + + if (\is_string($value)) { + return [Type::T_STRING, '"', [$value]]; + } + + throw new \InvalidArgumentException(sprintf('Cannot convert the value of type "%s" to a Sass value.', gettype($value))); + } +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Version.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Version.php new file mode 100644 index 0000000..62c8006 --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Version.php @@ -0,0 +1,23 @@ + + */ +class Version +{ + const VERSION = '1.8.1'; +} diff --git a/user/plugins/admin/vendor/scssphp/scssphp/src/Warn.php b/user/plugins/admin/vendor/scssphp/scssphp/src/Warn.php new file mode 100644 index 0000000..592b44c --- /dev/null +++ b/user/plugins/admin/vendor/scssphp/scssphp/src/Warn.php @@ -0,0 +1,84 @@ + ['onPluginsInitialized', 1000] + ]; + } + + /** + * Return `true` if the page has no extension, or has the default page extension. + * Return `false` if for example is a RSS version of the page + */ + private function isValidExtension() : bool + { + /** @var Uri $uri */ + $uri = $this->grav['uri']; + + $extension = $uri->extension(); + if (!$extension) { + return true; + } + + $disabled_extensions = (array)$this->grav['config']->get('plugins.advanced-pagecache.disabled_extensions'); + + return !in_array($extension, $disabled_extensions, true); + } + + /** + * Initialize configuration + */ + public function onPluginsInitialized(): void + { + /** @var Uri $uri */ + $uri = $this->grav['uri']; + + $config = (array)$this->grav['config']->get('plugins.advanced-pagecache'); + + $full_route = $uri->uri(); + $route = str_replace($uri->baseIncludingLanguage(), '', $full_route); + $params = $uri->params(null, true); + $query = $uri->query(null, true); + $user = $this->grav['user'] ?? null; + $lang = $this->grav['language']->getLanguageURLPrefix(); + + // Definitely don't run in admin plugin or is not a valid extension + if ($this->isAdmin() || !$this->isValidExtension()) { + return; + } + + // If this url is not whitelisted try some other tests + if (!in_array($route, (array)$config['whitelist'], true)) { + // do not run in these scenarios + if ( + ($config['disabled_with_params'] && !empty($params)) || + ($config['disabled_with_query'] && !empty($query)) || + ($config['disabled_on_login'] && ($user && $user["authenticated"])) || + in_array($route, (array)$config['blacklist'], true) + ) { + return; + } + } + + // Should run and store page + + if ($config['per_user_caching']) { + $this->pagecache_key = md5('adv-pc-' . $lang . $full_route . $user["username"]); + } else { + $this->pagecache_key = md5('adv-pc-' . $lang . $full_route); + } + + // TODO: remove when minimum required Grav >= 1.7.15 + if (version_compare($this->grav->getVersion(), '1.7.15', '<')) { + $this->enable([ + 'onOutputGenerated' => ['onOutputGenerated', -1000] + ]); + } else { + $this->enable([ + 'onOutputRendered' => ['onOutputRendered', 1000] + ]); + } + + $pageCache = $this->grav['cache']->fetch($this->pagecache_key); + if (is_array($pageCache)) { + $response = new Response($pageCache['code'], $pageCache['headers'], $pageCache['html']); + + $this->grav->close($response); + } + } + + /** + * Save the page to the cache + * TODO: remove when minimum required Grav >= 1.7.15 + */ + public function onOutputGenerated(Event $event): void + { + /** @var PageInterface $page */ + $page = $this->grav['page']; + $html = $this->grav->output; + + $item = [ + 'code' => $page->httpResponseCode(), + 'headers' => $page->httpHeaders(), + 'html' => $html, + ]; + + $this->grav['cache']->save($this->pagecache_key, $item); + } + + /** + * Save the page to the cache + */ + public function onOutputRendered(Event $event): void + { + /** @var PageInterface $page */ + $page = $event['page']; + $html = $event['output']; + + $item = [ + 'code' => $page->httpResponseCode(), + 'headers' => $page->httpHeaders(), + 'html' => $html, + ]; + + $this->grav['cache']->save($this->pagecache_key, $item); + } +} diff --git a/user/plugins/advanced-pagecache/advanced-pagecache.yaml b/user/plugins/advanced-pagecache/advanced-pagecache.yaml new file mode 100644 index 0000000..e4c30eb --- /dev/null +++ b/user/plugins/advanced-pagecache/advanced-pagecache.yaml @@ -0,0 +1,13 @@ +enabled: true # set to false to disable this plugin completely +disabled_with_params: true # disabled if there are params set on this URI (eg. /color:blue) +disabled_with_query: true # disabled if there are query options set on this URI (eg. ?color=blue) +disabled_on_login: true # disabled if a user is logged in on the frontend of the site +per_user_caching: false # enable per-user caching of pages (if not disabled_on_login) +disabled_extensions: [rss,xml,json] # disabled for these extensions +whitelist: # set to array of enabled page paths to enable only when in whitelist + - /cache-this-page +blacklist: # set to array and provide list of page paths to disable plugin for + - /error + - /login + - /random + - /dont-cache-this-page diff --git a/user/plugins/advanced-pagecache/blueprints.yaml b/user/plugins/advanced-pagecache/blueprints.yaml new file mode 100644 index 0000000..257ad1c --- /dev/null +++ b/user/plugins/advanced-pagecache/blueprints.yaml @@ -0,0 +1,106 @@ +name: AdvancedPageCache +slug: advanced-pagecache +type: plugin +version: 3.1.0 +description: "AdvancedPageCache turbo charges your site by statically caching pages." +icon: dashboard +author: + name: Team Grav + email: devs@getgrav.org + url: http://getgrav.org +homepage: https://github.com/getgrav/grav-plugin-advanced-pagecache +keywords: "static, cache, plugin, performance" +bugs: https://github.com/getgrav/grav-plugin-advanced-pagecache/issues +license: MIT + +dependencies: + - { name: grav, version: '>=1.7.0' } + +form: + validation: strict + fields: + + enabled: + type: toggle + label: Plugin Status + highlight: 1 + default: 1 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + + disabled_with_params: + type: toggle + label: Disabled with Params + highlight: 1 + default: 1 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + help: Disable caching if there are params set on the URI (eg. /color:blue) + + disabled_with_query: + type: toggle + label: Disabled with Query + highlight: 1 + default: 1 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + help: Disable caching if there are query options set on the URI (eg. ?color=blue) + + disabled_on_login: + type: toggle + label: Disabled for logged in users + highlight: 1 + default: 1 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + help: Disable caching if the user is logged in + + per_user_caching: + type: toggle + label: Per-user caching + highlight: 0 + default: 0 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + help: Enable per-user caching for logged in users (if caching is not disabled for logged in users) + + disabled_extensions: + type: selectize + size: large + placeholder: "e.g. rss, xml, json" + label: Disabled Extensions + help: URL extensions that should not be cached (e.g. for news feeds) + classes: fancy + validate: + type: commalist + + whitelist: + type: array + value_only: true + label: Whitelist + help: "Enable the plugin on these page paths regardless of other configuration options (don't include prefix, but do include extension)" + placeholder_key: + placeholder_value: /cache-this-route + + blacklist: + type: array + value_only: true + label: Blacklist + help: "Disable the plugin when calling these page paths (don't include language prefix, but do include extension)" + placeholder_key: + placeholder_value: /dont-cache-this-route diff --git a/user/plugins/aura/.gitignore b/user/plugins/aura/.gitignore new file mode 100644 index 0000000..dc1d402 --- /dev/null +++ b/user/plugins/aura/.gitignore @@ -0,0 +1,2 @@ +/.idea +/.vscode diff --git a/user/plugins/aura/CHANGELOG.md b/user/plugins/aura/CHANGELOG.md new file mode 100644 index 0000000..12037bd --- /dev/null +++ b/user/plugins/aura/CHANGELOG.md @@ -0,0 +1,77 @@ +# v2.1.2 +## 02-02-2021 + +1. [](#bugfix) +* Fix page image URL Issue #16 +* Fix double encoded HTML entities Issue #12 + +# v2.1.1 +## 02-02-2021 + +1. [](#bugfix) +* Fixed issue with initial save on new pages + +# v2.1.0 +## 15-12-2020 + +1. [](#new) + * Added support for flex pages in Grav 1.7 + +# v2.0.2 +## 03-11-2020 + +1. [](#bugfix) + * Fixed bug where custom metadata could not be removed + +# v2.0.1 +## 07-07-2020 + +1. [](#bugfix) + * Added check to include metadata saved prior to v2.0.0 + +1. [](#bugfix) + * Updated date published/modified functionality to ensure output of valid timestamp + +# v2.0.0 +## 23-06-2020 + +1. [](#improved) + * Changed the way metadata is stored in frontmatter to capitalise on Grav page caching. **Important:** When upgrading from a previous version existing Aura metadata output will be disabled. You will not be required to re-enter any information, but you will need to actively re-save each page via the page editor to re-enable metadata output. + +1. [](#new) + * Metadata input moved from Options tab to Aura tab in page editor for central editing location and to enable overriding of individual meta tags + +1. [](#new) + * Added support for individual author per page via Aura Authors plugin + +1. [](#bugfix) + * Changed storage location of Organization logo so it will be retained after plugin updates + +1. [](#bugfix) + * Fixed issue with URL extension appearing within page image URL + +# v1.0.3 +## 29-02-2020 + +1. [](#bugfix) + * Adjusted scope of autoloader so it will not interfere with other 'Aura' prefixed plugins + +# v1.0.2 +## 09-02-2020 + +1. [](#bugfix) + * Now appends to existing metadata rather than replacing + +# v1.0.1 +## 06-09-2019 + +1. [](#improved) + * Get language defined in page frontmatter with fallbacks to active language then default language +1. [](#bugfix) + * Adjusted JSON output to suit Grav versions > 1.5 + +# v1.0.0 +## 21-08-2019 + +1. [](#new) + * ChangeLog started... diff --git a/user/plugins/aura/LICENSE b/user/plugins/aura/LICENSE new file mode 100644 index 0000000..4a23cf9 --- /dev/null +++ b/user/plugins/aura/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Matt Mulhall + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/user/plugins/aura/README.md b/user/plugins/aura/README.md new file mode 100644 index 0000000..5d3de09 --- /dev/null +++ b/user/plugins/aura/README.md @@ -0,0 +1,42 @@ +# Aura Plugin + +The **Aura** Plugin for [Grav CMS](https://github.com/getgrav/grav) adds meta tags and structured data to your pages for visually appealing and informative search results and social media sharing. + +![Aura Plugin for Grav - Demo](assets/demo-composition-min.png) + +## Features + +* Minimal configuration required to automatically inject a wealth of structured/meta data into all pages of your site. +* Supports the following standards and protocols: + * [Schema.org](https://schema.org/) structured data ld+json code snippet as used by major search providers Google, Microsoft, Yahoo! and Yandex + * [Open Graph](https://ogp.me/) meta tags as used by social platforms Facebook and Pinterest + * [Twitter Card](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards.html) meta tags + * [LinkedIn Article](https://www.linkedin.com/help/linkedin/answer/46687/making-your-website-shareable-on-linkedin?lang=en) meta tags + +## Installation + +### Admin Plugin + +It is recommended to install Aura directly through the Admin Plugin by browsing to the `Plugins` tab and selecting `Add`. + +**Important:** If upgrading from a version < 2.0.0 existing Aura metadata output will be disabled due to a change in the way the metadata is stored. You will not be required to re-enter any information, but you will need to actively re-save each page via the page editor to re-enable metadata output. The new method of saving metadata capitalises on Grav's inbuilt page caching for optimal site performance. + +## Configuration + +You can configure Aura via the Admin Plugin by browsing to `Plugins` and selecting `Aura`. + +There are only two required fields to complete to get up and running: Organization Name and URL. However the more information you can provide, the richer your shared content will be. + +## Usage + +Global settings are configured at the plugin level as described above. These values are constant for each page of your site e.g. Organization name, logo and social account details. + +There is an additional configuration tab titled `Aura` available on each page in the page editor. This is where to set page specific information such as the meta description. Other than meta description, data required at a page level (URL, title etc.) will be automatically inferred from Grav's internal system settings. + +Additional metadata can also be entered on this tab. For example to signal to web crawlers not to index the page, enter key: `robots` and value: `noindex,nofollow`. + +Individual Aura generated meta tags can be overridden with the Additional Metadata input as well. For example to display a specific title when sharing on Twitter only, enter a custom value for the key `twitter:title`. + +## Credits + +Big thanks to [Yoast](https://yoast.com/) for sharing their wealth of knowledge on structured data and SEO. diff --git a/user/plugins/aura/assets/demo-composition-min.png b/user/plugins/aura/assets/demo-composition-min.png new file mode 100644 index 0000000000000000000000000000000000000000..221c7a3522017810498649e5b7504a18bb94af99 GIT binary patch literal 135930 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¨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

    JdEF$abtcR54u5r}_Vsh}P<`X}; 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 ['onPluginsInitialized', 0], + ]; + } + + /** + * Initialize the plugin + */ + public function onPluginsInitialized() + { + spl_autoload_register( + function ($class) { + if (Utils::startsWith($class, 'Grav\Plugin\Aura\\')) { + require_once __DIR__ . '/classes/' . strtolower(basename(str_replace("\\", '/', $class))) . '.php'; + } + } + ); + + // Admin only events + if ($this->isAdmin()) { + $this->enable( + [ + 'onGetPageBlueprints' => ['onGetPageBlueprints', 0], + 'onAdminSave' => ['onAdminSave', 0], + ] + ); + return; + } + + // Frontend events + $this->enable( + [ + 'onPageInitialized' => ['onPageInitialized', 0], + ] + ); + } + + /** + * Extend page blueprints with additional configuration options. + * + * @param Event $event + */ + public function onGetPageBlueprints($event) + { + $types = $event->types; + $types->scanBlueprints('plugins://' . $this->name . '/blueprints'); + } + + public function onAdminSave(Event $event) + { + if (!$event['object'] instanceof PageObject) { + return; + } + + // Don't proceed if required params not set + $requiredParams = [ + 'org-name', + 'org-url', + ]; + + foreach ($requiredParams as $param) { + $key = 'plugins.aura.' . $param; + + if (!$this->grav['config']->get($key)) { + return; + } + } + + $page = $event['object']; + $aura = new Aura($page); + + // Meta Description + if ($aura->webpage->description) { + // Append description to page metadata + $aura->webpage->metadata['description'] = [ + 'name' => 'description', + 'content' => $aura->webpage->description, + ]; + } + + // Open Graph + if ($this->grav['config']->get('plugins.aura.output-og')) { + $aura->generateOpenGraphMeta(); + } + + // Twitter + if ($this->grav['config']->get('plugins.aura.output-twitter')) { + $aura->generateTwitterMeta(); + } + + // LinkedIn + if ($this->grav['config']->get('plugins.aura.output-linkedin')) { + $aura->generateLinkedInMeta(); + } + + // Generate Aura metadata + $metadata = []; + + foreach ($aura->webpage->metadata as $tag) { + if (array_key_exists('property', $tag)) { + $metadata[$tag['property']] = $tag['content']; + } elseif (array_key_exists('name', $tag)) { + $metadata[$tag['name']] = $tag['content']; + } + } + + $original = $page->getOriginal(); + + if ($original && !isset($original->header()->aura) && isset($page->header()->metadata) && is_array($page->header()->metadata)) { + // Page has not been saved since installation of Aura and includes some custom metadata + $legacyMetadata = ['metadata' => []]; + + foreach ($page->header()->metadata as $key => $val) { + if (!array_key_exists($key, $metadata)) { + // A new value has not been supplied via Aura, salvage existing metadata + $metadata[$key] = $val; + $legacyMetadata['metadata'][$key] = $val; + } + } + + $page->header()->aura = array_merge($legacyMetadata, $page->header()->aura ?? []); + } + + $page->header()->metadata = array_merge($metadata, $page->header()->aura['metadata'] ?? []); + } + + /** + * Insert meta tags and structured data to head of each page + */ + public function onPageInitialized() + { + // Structured Data + if (!$this->grav['config']->get('plugins.aura.output-sd')) { + return; + } + + // Don't proceed if required params not set + $requiredParams = [ + 'org-name', + 'org-url', + ]; + + foreach ($requiredParams as $param) { + $key = 'plugins.aura.' . $param; + + if (!$this->grav['config']->get($key)) { + return; + } + } + + $page = $this->grav['page']; + $assets = $this->grav['assets']; + + $aura = new Aura($page); + + // Generate structured data block + $sd = $aura->generateStructuredData(); + + // Drop into JS pipeline + $assets->addInlineJs($sd, null, null, ['type' => 'application/ld+json']); + } +} diff --git a/user/plugins/aura/aura.yaml b/user/plugins/aura/aura.yaml new file mode 100644 index 0000000..d4ca941 --- /dev/null +++ b/user/plugins/aura/aura.yaml @@ -0,0 +1 @@ +enabled: true diff --git a/user/plugins/aura/blueprints.yaml b/user/plugins/aura/blueprints.yaml new file mode 100644 index 0000000..9b2a1cd --- /dev/null +++ b/user/plugins/aura/blueprints.yaml @@ -0,0 +1,142 @@ +name: Aura +slug: aura +type: plugin +version: 2.1.2 +description: Automatically add meta tags and structured data to your pages for visually appealing and informative search results and social media sharing. +icon: code +author: + name: Matt Mulhall + email: matt@theskylab.net + url: https://www.theskylab.net +homepage: https://github.com/matt-j-m/grav-plugin-aura +keywords: seo, structured data, open graph +bugs: https://github.com/matt-j-m/grav-plugin-aura/issues +license: MIT + +dependencies: + - { name: grav, version: '>=1.7.0' } + +form: + validation: strict + fields: + enabled: + type: toggle + label: PLUGIN_ADMIN.PLUGIN_STATUS + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + output-heading: + type: section + title: Output Options + text: 'Enable any or all of the following options to include the relevant tags and metadata in each page.' + underline: true + output-sd: + type: toggle + label: schema.org Structured Data (Google, Microsoft, Yahoo!, Yandex) + highlight: 1 + default: 1 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + output-og: + type: toggle + label: Open Graph (Facebook, Pinterest) + highlight: 1 + default: 1 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + output-twitter: + type: toggle + label: Twitter Card + highlight: 1 + default: 1 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + output-linkedin: + type: toggle + label: LinkedIn Article + highlight: 1 + default: 1 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + org-heading: + type: section + title: Organization Information + underline: true + org-name: + type: text + size: large + label: Name + validate: + required: true + org-url: + type: text + size: large + label: URL + validate: + required: true + org-logo: + type: file + size: large + label: Logo (minimum 112x112px .jpg, .png or .gif format only) + multiple: false + destination: 'user/images' + accept: + - .jpg + - .png + - .gif + org-facebook-url: + type: text + size: large + label: Facebook URL + placeholder: 'https://www.facebook.com/username' + org-facebook-appid: + type: text + size: large + label: Facebook App ID + placeholder: '1234567890' + org-twitter-user: + type: text + size: large + label: Twitter Username + placeholder: 'username' + org-instagram-url: + type: text + size: large + label: Instagram URL + placeholder: 'https://www.instagram.com/username/' + org-linkedin-url: + type: text + size: large + label: LinkedIn URL + placeholder: 'https://www.linkedin.com/company/company-name/' + org-pinterest-url: + type: text + size: large + label: Pinterest URL + placeholder: 'https://www.pinterest.com/username/' + org-youtube-url: + type: text + size: large + label: YouTube URL + placeholder: 'https://www.youtube.com/username' + org-wikipedia-url: + type: text + size: large + label: Wikipedia URL + placeholder: "https://en.wikipedia.org/wiki/Company" diff --git a/user/plugins/aura/blueprints/default.yaml b/user/plugins/aura/blueprints/default.yaml new file mode 100644 index 0000000..37e5b68 --- /dev/null +++ b/user/plugins/aura/blueprints/default.yaml @@ -0,0 +1,61 @@ +title: Aura +'@extends': + type: default + context: blueprints://pages + +form: + fields: + tabs: + type: tabs + active: 1 + + fields: + + options: + type: tab + title: PLUGIN_ADMIN.OPTIONS + + fields: + publishing: + type: section + title: PLUGIN_ADMIN.PUBLISHING + underline: true + + fields: + header.metadata: + unset@: true + + aura: + type: tab + title: Aura + + fields: + + header.aura.pagetype: + type: select + label: Page Type + size: medium + options: + website: Website (default) + article: Article + validate: + required: true + + header.aura.description: + type: text + size: long + label: 'Page Description' + description: 'Recommended length 140-160 characters.' + + header.aura.image: + type: filepicker + label: Preview Image + preview_images: true + description: 'Recommended size 1920x1080px in .jpg, .png or .gif format only. If not specified, will default to the first image found in the page''s folder.' + + header.aura.metadata: + toggleable: true + type: array + label: Additional Metadata + placeholder_key: PLUGIN_ADMIN.METADATA_KEY + placeholder_value: PLUGIN_ADMIN.METADATA_VALUE diff --git a/user/plugins/aura/classes/aura.php b/user/plugins/aura/classes/aura.php new file mode 100644 index 0000000..066c72a --- /dev/null +++ b/user/plugins/aura/classes/aura.php @@ -0,0 +1,535 @@ +grav = $cache = Grav::instance(); + + $this->setOrg(); + $this->setWebsite(); + $this->setWebPage($page); + $this->setAuthor($page); + } + + public function generateOpenGraphMeta() + { + $data = [ + 'og:url' => $this->webpage->url, + 'og:type' => $this->webpage->type, + 'og:title' => $this->webpage->title, + ]; + + if ($this->webpage->description) { + $data['og:description'] = $this->webpage->description; + } + + if ($this->webpage->image) { + $data['og:image'] = $this->webpage->image->url; + $data['og:image:type'] = $this->webpage->image->type; + $data['og:image:width'] = $this->webpage->image->width; + $data['og:image:height'] = $this->webpage->image->height; + } + + if ($this->grav['config']->get('plugins.aura.org-facebook-appid')) { + $data['fb:app_id'] = $this->grav['config']->get('plugins.aura.org-facebook-appid'); + } + + if ($this->person) { + $data['og:author'] = $this->person->name; + } else { + $data['og:author'] = $this->org->name; + } + + foreach ($data as $property => $content) { + $this->webpage->metadata[$property] = [ + 'property' => $property, + 'content' => $content, + ]; + } + } + + public function generateTwitterMeta() + { + $data = [ + 'twitter:card' => 'summary_large_image', + 'twitter:title' => $this->webpage->title, + ]; + + if ($this->webpage->description) { + $data['twitter:description'] = $this->webpage->description; + } + + if ($this->grav['config']->get('plugins.aura.org-twitter-user')) { + $data['twitter:site'] = '@' . $this->grav['config']->get('plugins.aura.org-twitter-user'); + } + + if ($this->person && $this->person->twitterUser) { + $data['twitter:creator'] = '@' . $this->person->twitterUser; + } else { + if ($this->grav['config']->get('plugins.aura.org-twitter-user')) { + $data['twitter:creator'] = '@' . $this->grav['config']->get('plugins.aura.org-twitter-user'); + } + } + + if ($this->webpage->image) { + $data['twitter:image'] = $this->webpage->image->url; + } + + foreach ($data as $name => $content) { + $this->webpage->metadata[$name] = [ + 'name' => $name, + 'content' => $content, + ]; + } + } + + public function generateLinkedInMeta() + { + $data = [ + 'article:published_time' => $this->webpage->datePublished, + 'article:modified_time' => $this->webpage->dateModified, + ]; + + if ($this->person) { + $data['article:author'] = $this->person->name; + } else { + $data['article:author'] = $this->org->name; + } + + foreach ($data as $property => $content) { + $this->webpage->metadata[$property] = [ + 'property' => $property, + 'content' => $content, + ]; + } + } + + public function generateStructuredData() + { + $organization = [ + '@type' => 'Organization', + '@id' => $this->org->id, + 'name' => $this->org->name, + 'url' => $this->org->url, + ]; + + $website = [ + '@type' => 'WebSite', + '@id' => $this->website->id, + 'url' => $this->website->url, + 'name' => $this->website->name, + 'publisher' => [ + '@id' => $this->org->id, + ], + ]; + + $webpage = [ + '@type' => 'WebPage', + '@id' => $this->webpage->id, + 'url' => $this->webpage->url, + 'inLanguage' => $this->webpage->language, + 'name' => $this->webpage->title, + 'isPartOf' => [ + '@id' => $this->website->id, + ], + 'datePublished' => $this->webpage->datePublished, + 'dateModified' => $this->webpage->dateModified, + ]; + + // Add Organization sameAs (if defined) + if ($this->org->sameAs) { + $organization['sameAs'] = $this->org->sameAs; + } + + // Add logo (if defined) + if ($this->org->logo) { + $organization['logo'] = [ + '@type' => 'ImageObject', + '@id' => $this->org->logo->id, + 'url' => $this->org->logo->url, + 'width' => $this->org->logo->width, + 'height' => $this->org->logo->height, + 'caption' => $this->org->logo->caption, + ]; + + $organization['image'] = [ + '@id' => $this->org->logo->id, + ]; + } + + // Add page description (if defined) + if ($this->webpage->description) { + $webpage['description'] = $this->webpage->description; + } + + // Add page image (if defined) + if ($this->webpage->image) { + $webpageImage = [ + '@type' => 'ImageObject', + '@id' => $this->webpage->image->id, + 'url' => $this->webpage->image->url, + 'width' => $this->webpage->image->width, + 'height' => $this->webpage->image->height, + 'caption' => $this->webpage->image->caption, + ]; + + $webpage['primaryImageOfPage'] = [ + '@id' => $this->webpage->image->id, + ]; + } + + // Additional based on page type i.e. article + if ($this->webpage->type == 'article') { + $article = [ + '@type' => 'Article', + '@id' => $this->webpage->url . '#article', + 'isPartOf' => [ + '@id' => $this->webpage->id, + ], + 'headline' => $this->webpage->title, + 'datePublished' => $this->webpage->datePublished, + 'dateModified' => $this->webpage->dateModified, + 'mainEntityOfPage' => [ + '@id' => $this->webpage->id, + ], + 'publisher' => [ + '@id' => $this->org->id, + ], + ]; + + // Add Image + if ($this->webpage->image) { + $article['image'] = [ + '@id' => $this->webpage->image->id, + ]; + } + + // Add Author + if ($this->person) { + // Use Person (if defined) + $person = [ + '@type' => 'Person', + '@id' => $this->person->id, + 'name' => $this->person->name, + ]; + + // Add Person description (if defined) + if ($this->person->description) { + $person['description'] = $this->person->description; + } + + // Add Person sameAs (if defined) + if ($this->person->sameAs) { + $person['sameAs'] = $this->person->sameAs; + } + + // Add Person image (if defined) + if ($this->person->image) { + $person['image'] = [ + '@type' => 'ImageObject', + '@id' => $this->person->image->id, + 'url' => $this->person->image->url, + 'width' => $this->person->image->width, + 'height' => $this->person->image->height, + 'caption' => $this->person->image->caption, + ]; + } + + $article['author'] = [ + '@id' => $this->person->id, + ]; + } else { + // Use Organization + $article['author'] = [ + '@id' => $this->org->id, + ]; + } + } + + // Build the empty structured data block + $data = [ + '@context' => 'https://schema.org', + '@graph' => [], + ]; + + // Add the elements in order + $data['@graph'][] = $organization; + $data['@graph'][] = $website; + + if (isset($webpageImage)) { + $data['@graph'][] = $webpageImage; + } + + $data['@graph'][] = $webpage; + + if (isset($article)) { + $data['@graph'][] = $article; + } + + if (isset($person)) { + $data['@graph'][] = $person; + } + + return json_encode($data, JSON_UNESCAPED_SLASHES); + } + + /** + * @return void + */ + protected function setOrg(): void + { + $this->org = new Organization(); + + $this->org->url = (string)$this->grav['config']->get('plugins.aura.org-url'); + $this->org->id = $this->org->url . '#organization'; + $this->org->name = $this->grav['config']->get('plugins.aura.org-name'); + + // Org SameAs + $sameAs = []; + + foreach ($this->otherPresence as $platform) { + $key = 'plugins.aura.org-' . $platform; + + if ($this->grav['config']->get($key)) { + $sameAs[] = $this->grav['config']->get($key); + } + } + + $key = 'plugins.aura.org-twitter-user'; + + if ($this->grav['config']->get($key)) { + $sameAs[] = 'https://twitter.com/' . $this->grav['config']->get($key); + } + + if (!empty($sameAs)) { + $this->org->sameAs = $sameAs; + } + + $this->setOrgLogo(); + } + + /** + * @return void + */ + protected function setOrgLogo(): void + { + // Org Logo + if ($this->grav['config']->get('plugins.aura.org-logo')) { + $imageArray = $this->grav['config']->get('plugins.aura.org-logo'); + $firstImage = reset($imageArray); + $imagePath = ROOT_DIR . $firstImage['path']; + + if (file_exists($imagePath)) { + $size = getimagesize($imagePath); + $this->org->logo = new Image(); + $this->org->logo->url = $this->grav['base_url_absolute'] . '/' . $firstImage['path']; + $this->org->logo->id = $this->org->url . '#logo'; + $this->org->logo->width = $size[0]; + $this->org->logo->height = $size[1]; + $this->org->logo->caption = $this->org->name; + $this->org->logo->type = $size['mime']; + } + } + } + + /** + * @return void + */ + protected function setWebsite(): void + { + $this->website = new WebSite; + + $this->website->url = $this->grav['base_url_absolute']; + $this->website->id = $this->website->url . '#website'; + $this->website->name = $this->grav['config']->get('site.title'); + } + + /** + * @param PageObject $page + * + * @return mixed + */ + protected function setWebPage($page) + { + $this->webpage = new WebPage; + + $this->webpage->url = $page->url(true); + $this->webpage->id = $this->webpage->url . '#webpage'; + $this->webpage->title = $page->title() . ' | ' . $this->grav['config']->get('site.title'); + + $header = $page->header(); + + if (isset($header->aura['description']) && $header->aura['description'] != '') { + $this->webpage->description = (string)$header->aura['description']; + } + + if (isset($header->language) && $header->language != '') { + $this->webpage->language = $header->language; + } else { + $this->webpage->language = $this->grav['language']->getActive(); + + if (!$this->webpage->language) { + $this->webpage->language = $this->grav['config']->get('site.default_lang'); + } + } + + $datePublishedRaw = time(); + + if ($page->publishDate()) { + $datePublishedRaw = $page->publishDate(); + } elseif ($page->date()) { + $datePublishedRaw = $page->date(); + } elseif ($page->modified()) { + $datePublishedRaw = $page->modified(); + } + + $dateModifiedRaw = $page->modified() ? $page->modified() : time(); + $this->webpage->datePublished = date('c', $datePublishedRaw); + $this->webpage->dateModified = date('c', $dateModifiedRaw); + + $this->setWebpageImage($page); + + if ((isset($header->aura['pagetype'])) && ($header->aura['pagetype'] != '')) { + $this->webpage->type = $header->aura['pagetype']; + } + } + + /** + * @param PageObject $page + * + * @return void + */ + protected function setWebpageImage($page): void + { + $image = null; + $header = $page->header(); + + if (isset($header->aura['image']) && $header->aura['image'] !== '') { + $image = $page->media()->images()[$header->aura['image']] ?? null; + } elseif (isset($header->media_order) && $header->media_order !== '') { + $images = explode(',', $header->media_order); + + if (is_array($images) && !empty($images)) { + $image = $page->media()->images()[reset($images)] ?? null; + } + } + + if ($image !== null) { + $url_data = array_merge( + parse_url($this->grav->get('base_url_absolute')), + parse_url($image->url()) + ); + + $url = call_user_func( + static function (array $parts) { + return (isset($parts['scheme']) ? "{$parts['scheme']}:" : '') . + ((isset($parts['user']) || isset($parts['host'])) ? '//' : '') . + (isset($parts['user']) ? (string)($parts['user']) : '') . + (isset($parts['pass']) ? ":{$parts['pass']}" : '') . + (isset($parts['user']) ? '@' : '') . + (isset($parts['host']) ? (string)($parts['host']) : '') . + (isset($parts['port']) ? ":{$parts['port']}" : '') . + (isset($parts['path']) ? (string)($parts['path']) : '') . + (isset($parts['query']) ? "?{$parts['query']}" : '') . + (isset($parts['fragment']) ? "#{$parts['fragment']}" : ''); + }, $url_data + ); + + $this->webpage->image = new Image(); + $this->webpage->image->id = $this->webpage->url . '#primaryimage'; + $this->webpage->image->caption = $this->webpage->title; + $this->webpage->image->url = $url; + $this->webpage->image->width = $image->get('width'); + $this->webpage->image->height = $image->get('height'); + $this->webpage->image->type = $image->get('mime'); + } + } + + /** + * @param PageObject $page + */ + protected function setAuthor($page): void + { + if (!$this->grav['config']->get('plugins.aura-authors.enabled') || !isset($page->header()->aura['author'])) { + return; + } + + $authors = $this->grav['config']->get('plugins.aura-authors.authors'); + $key = array_search($page->header()->aura['author'], array_column($authors, 'label')); + + if ($key !== false) { + $author = $authors[$key]; + + $this->person = new Person(); + $this->person->id = $this->org->url . '#person/' . $author['label']; + $this->person->name = $author['name']; + $this->person->description = $author['description']; + + // Person SameAs + $sameAs = []; + + foreach ($this->otherPresence as $platform) { + $key = 'person-' . $platform; + + if (isset($author[$key]) && $author[$key] != '') { + $sameAs[] = $author[$key]; + } + } + + $key = 'person-twitter-user'; + + if (isset($author[$key]) && $author[$key] != '') { + $this->person->twitterUser = $author[$key]; + $sameAs[] = 'https://twitter.com/' . $author[$key]; + } + + if (!empty($sameAs)) { + $this->person->sameAs = $sameAs; + } + + // Person Image + if (isset($author['image']) && !empty($author['image'])) { + $firstImage = reset($author['image']); + $imagePath = ROOT_DIR . $firstImage['path']; + + if (file_exists($imagePath)) { + $size = getimagesize($imagePath); + $this->person->image = new Image(); + $this->person->image->url = $this->grav['base_url_absolute'] . '/' . $firstImage['path']; + $this->person->image->id = $this->org->url . '#personimage/' . $author['label']; + $this->person->image->width = $size[0]; + $this->person->image->height = $size[1]; + $this->person->image->caption = $author['name']; + $this->person->image->type = $size['mime']; + } + } + } + } +} diff --git a/user/plugins/aura/classes/image.php b/user/plugins/aura/classes/image.php new file mode 100644 index 0000000..c85dcba --- /dev/null +++ b/user/plugins/aura/classes/image.php @@ -0,0 +1,12 @@ +=7.1.3", + "ext-json": "*" + }, + "autoload": { + "psr-4": { + "Grav\\Plugin\\Aura\\": "classes/" + }, + "classmap": [ + "aura.php" + ] + }, + "config": { + "platform": { + "php": "7.1.3" + } + } +} diff --git a/user/plugins/automagic-images/CHANGELOG.md b/user/plugins/automagic-images/CHANGELOG.md new file mode 100644 index 0000000..5aa4fff --- /dev/null +++ b/user/plugins/automagic-images/CHANGELOG.md @@ -0,0 +1,42 @@ +# v1.0.5 +## 14-06-2021 + +1. [](#bugfix) + * included vendor code… + +# v1.0.4 +## 11-06-2021 + +1. [](#bugfix) + * removed debugging code + +# v1.0.3 +## 10-06-2021 + +1. [](#improved) + * reverted the output changes because they didn't work for twig-generated content + +# v1.0.2 +## 27-05-2021 + +1. [](#bugfix) + * fixed a bit of rogue code from the last update +2. [](#improved) + * added some more information in the README about Grav’s resizing bugs + + +# v1.0.1 +## 25-05-2021 + +1. [](#improved) + * made the output changes that add the sizes attributes cacheable + + +# v1.0 +## 10-05-2021 + +1. [](#new) + * Initial release as a fork from Fredrik Ekelund’s plugin Resize Images, which seems unmaintained +2. [](#improved) + * added translation file and German translation for Admin settings + * adapted code from Ole Vik’s plugin Image Scrset to allow sizes attributes to be set per CSS class in the plugin settings diff --git a/user/plugins/automagic-images/LICENSE b/user/plugins/automagic-images/LICENSE new file mode 100644 index 0000000..3a9cb4a --- /dev/null +++ b/user/plugins/automagic-images/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Anna Kleiner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/user/plugins/automagic-images/README.md b/user/plugins/automagic-images/README.md new file mode 100644 index 0000000..f2709bc --- /dev/null +++ b/user/plugins/automagic-images/README.md @@ -0,0 +1,97 @@ +# [Grav](http://getgrav.org) Automagic Images Plugin + +This plugin started out as a fork from the most excellent [Resize Images](https://github.com/fredrikekelund/grav-plugin-resize-images) by Fredrik Ekelund. That seems unmaintained however, and I needed some image functionality in my projects that I felt would be best bundled up in one plugin. + +## Currently it does two things: + +1. It resizes a page’s images when you save that page in Admin, according to sizes that can be customised in the plugin’s settings. + +For example, if you keep the default settings and upload a 4000x3000 example-image.jpg to a page, upon clicking save it will generate the following versions: +- example-image.jpg (640x480) +- example-image@2x.jpg (1000x750) +- example-image@3x.jpg (1500x1125) +- example-image@4x.jpg (2500x1875) +- example-image@5x.jpg (3500x2625) +- example-image@6x.jpg (4000x3000, original) + +If you set `remove original` to `on`, the last version will be deleted instead. + +In Grav Admin you will see only the smallest file, but in your file system all the larger versions are still in the same page folder. This is a built-in functionality of Grav which relies on the `@2x` naming convention. + + +2. It adds a `sizes` directive to your images, according to their CSS class. + +*This will not do anything if you don’t set any sizes for your image classes, or if your images do not have classes and you do not define sizes for a default class.* + +Let’s say for example that you have some images which all have a class `content-pic` that sets them at 100% width for mobile, at 33% width if the viewport is at least 37.5em (600px) wide, and at 25em width as soon as the viewport reaches 75em (1200px). In this case the sizes settings should be: + +```yaml +sizesattr: + - + class: 'content-pic' + directive: '(min-width: 75em) 25em, (min-width: 37.5em) 33%, 100vw' +``` + +In short, whatever you put in that `directive` will show up directly in your html as a `sizes=""` attribute for images with that class. + +If *all* your images are the same size, you could also just set a `default` class which gets applied to any images without a class attribute that has a corresponding sizes setting in the plugin. **The `srcset` attribute will be generated by Grav’s built-in functionality from all the available image variants.** + +### A note on performance issues +If you use `cropZoom` or similar on an image, this has nothing to do with the plugin. Whatever you upload gets resized *and saved in the page’s folder* according to your sizes settings and the original image ratio. The images that are generated by using `cropZoom` are stored in Grav’s image cache and have to be re-generated whenever that cache is emptied. Since with this plugin you have several versions of each image which **ALL** get cropZoomed, this can take a while and on pages with a lot of images may result in timeout errors on a cache refresh. In that case you can just reload the page until all the images have been generated and cached. + +Same goes for pages where you add twenty images with 7 MB each. That will almost certainly result in a Crikey! timeout error in the Admin upon clicking save – but you can just hit reload until all images have been processed. (I have personally done this for pages with *dozens* of large images, it took several minutes for a timeout after 30 seconds on a regular shared hosting server, and all was fine afterwards.) + +### Another note on using `cropZoom` and the like +When there are several different-sized versions of an image present, that seems to throw off Grav’s internal calculations and cropZoomed images will end up a *different* size than what you asked for. This is not the plugin’s fault, if you add your own @2x versions of an image without using this plugin you will run into this issue as well. And as far as I can tell, the sizes of the cropZoomed versions in Grav’s cache are all over the place, more wildly so the larger they are. This is a bug in Grav, unfortunately. I recommend checking your own use case, it may not always be a problem but currently it’s often best not to mix optimised images and programmatic reshaping. :-( + +## Why not use Grav’s built-in functionality? +In theory, Grav has all the image-handling capabilities I’d personally need ([see the docs on page media](https://learn.getgrav.org/16/content/media)). However, there are three main reasons why I use this plugin: + +1. If you have the Imagick module available, this plugin can use it, resulting in better image quality. +2. There are [currently some bugs](https://github.com/getgrav/grav/issues/3146) in the native image handling system that make it inadvisable to mix cropZoom and the like with setting `sizes` (both in Twig and Markdown). +3. When doing projects for clients, I like to keep things as simple as possible for them, which ideally is “upload an image and Grav does the rest”. If an image is used in Markdown (I try to avoid that as well, but it’s not always possible), I don’t want people to have to deal with setting sizes. + +So in short, if you want to lovingly handcraft every image on your site, you do not need this plugin. If you want to set up a system where in the end, you upload an image to a page, click save, and then it just works and the result is fairly good, this is the plugin for you. + +A note: Images that already have responsive alternatives with the “@2x” naming convention won’t be resized, so you can also do your own image tweaking if you like. + +## Configuration + +You can customize the set of widths that your images will be resized to. By default they are 640, 1000, 1500, 2500, 3500 pixels in width. Images will never be scaled up, however, so only the widths that are smaller than the original image’s will be used. + +For every width, you’re also able to set the JPEG compression quality. A good rule of thumb is to lower that number at higher widths - the result will still be good! + +This plugin won’t convert PNGs to JPEGs, so the quality number only applies to JPEG images. + +To generate variations of existing images go into the admin panel and re-save the pages where those images live. Every time a page is saved (whether it’s new or old), this plugin will go through all images (again, whether they are new or old) in that page, check if they have responsive variants and generate new ones if necessary. + +## Installation + +Installing the Automagic Images plugin can be done in one of three ways: The GPM (Grav Package Manager) installation method lets you quickly install the plugin with a simple terminal command, the manual method lets you do so via a zip file, and the admin method lets you do so via the Admin Plugin. + +### GPM Installation (Preferred) + +To install the plugin via the [GPM](http://learn.getgrav.org/advanced/grav-gpm), through your system’s terminal (also called the command line), navigate to the root of your Grav-installation, and enter: + + bin/gpm install automagic-images + +This will install the Automagic Images plugin into your `/user/plugins`-directory within Grav. Its files can be found under `/your/site/grav/user/plugins/automagic-images`. + +### Manual Installation + +To install the plugin manually, download the zip-version of this repository and unzip it under `/your/site/grav/user/plugins`. Then rename the folder to `automagic-images`. You can find these files on [GitHub](https://github.com/skinofthesoul/grav-plugin-automagic-images) or via [GetGrav.org](http://getgrav.org/downloads/plugins). + +You should now have all the plugin files under + + /your/site/grav/user/plugins/automagic-images + +### Admin Plugin + +If you use the Admin Plugin, you can install the plugin directly by browsing the `Plugins`-menu and clicking on the `Add` button. + +## Many thanks go to +@fredrikekelund – all credit for the actual code that does all the resizing goes to him. +@olevik – copying and adapting his code from Image Srcset to add the `sizes` per CSS class was a lot easier than writing it all by myself, and also I wouldn’t have learned about the excellent [PHP Html Parser](https://github.com/paquettg/php-html-parser) otherwise. + +## Plans for the future +Being able to regenerate ALL images with a click would be incredibly nice, but I really don’t know when I might get around to that. Co-maintainers and -developers are welcome! Also adding support for formats other than jpg and png, that seems easier to do and will be next. \ No newline at end of file diff --git a/user/plugins/automagic-images/adapters/gd.php b/user/plugins/automagic-images/adapters/gd.php new file mode 100644 index 0000000..75b473a --- /dev/null +++ b/user/plugins/automagic-images/adapters/gd.php @@ -0,0 +1,113 @@ +original_width = $size[0]; + $this->original_height = $size[1]; + + if (preg_match('/jpe?g/', $extension)) { + $this->image = imagecreatefromjpeg($source); + $this->format = 'JPEG'; + } else if ($extension == 'png') { + $this->image = imagecreatefrompng($source); + $this->format = 'PNG'; + } + + return $this; + } + + /** + * Gets the image format + * @return string - Either 'JPEG' or 'PNG' + */ + public function getFormat() + { + return $this->format; + } + + /** + * Resizes the image to the specified dimensions + * @param float $width + * @param float $height + * @return GDAdapter - Returns $this + */ + public function resize($width, $height) + { + $this->target = imagecreatetruecolor($width, $height); + $format = $this->getFormat(); + + if ($format == 'PNG') { + $transparent = imagecolorallocatealpha($this->target, 255, 255, 255, 127); + + imagealphablending($this->target, false); + imagesavealpha($this->target, true); + imagefilledrectangle($this->target, 0, 0, $width, $height, $transparent); + } + + imagecopyresampled($this->target, $this->image, 0, 0, 0, 0, $width, $height, $this->original_width, $this->original_height); + + return $this; + } + + /** + * Sets JPEG quality of target image + * @param int $quality + * @return GDAdapter - Returns $this + */ + public function setQuality($quality) + { + $this->quality = $quality; + + return $this; + } + + /** + * Generates image and saves it to disk + * @param string $filename - Target filename for image + * @return bool - Returns true if successful, false otherwise + */ + public function save($filename) + { + $format = $this->getFormat(); + + if ($format == 'JPEG') { + $result = imagejpeg($this->target, $filename, $this->quality); + } else if ($format == 'PNG') { + $result = imagepng($this->target, $filename, 9); + } + + imagedestroy($this->image); + imagedestroy($this->target); + + return $result; + } +} diff --git a/user/plugins/automagic-images/adapters/imagick.php b/user/plugins/automagic-images/adapters/imagick.php new file mode 100644 index 0000000..92dac8d --- /dev/null +++ b/user/plugins/automagic-images/adapters/imagick.php @@ -0,0 +1,82 @@ +image = new \Imagick($source); + $this->format = strtolower($this->image->getImageFormat()); + + return $this; + } + + /** + * Gets the image format + * @return string - Either 'JPEG' or 'PNG' + */ + public function getFormat() + { + return $this->format; + } + + /** + * Resizes the image to the specified dimensions + * @param float $width + * @param float $height + * @return ImagickAdapter - Returns $this + */ + public function resize($width, $height) + { + $this->image->resizeImage($width, $height, \Imagick::FILTER_LANCZOS, 1); + + return $this; + } + + /** + * Sets JPEG quality of target image + * @param int $quality + * @return ImagickAdapter - Returns $this + */ + public function setQuality($quality) + { + $this->image->setImageCompressionQuality($quality); + + return $this; + } + + /** + * Generates image and saves it to disk + * @param string $filename - Target filename for image + * @return bool - Returns true if successful, false otherwise + */ + public function save($filename) + { + $format = $this->getFormat(); + + if ($format == 'jpeg') { + $this->image->setImageCompression(\Imagick::COMPRESSION_JPEG); + } else if ($format == 'png') { + $this->image->setImageCompression(\Imagick::COMPRESSION_ZIP); + } + + $result = $this->image->writeImage($filename); + $this->image->clear(); + + return (bool) $result; + } +} diff --git a/user/plugins/automagic-images/adapters/interface.php b/user/plugins/automagic-images/adapters/interface.php new file mode 100644 index 0000000..986bda1 --- /dev/null +++ b/user/plugins/automagic-images/adapters/interface.php @@ -0,0 +1,15 @@ + ['onAdminSave', 0], + 'onOutputGenerated' => ['onOutputGenerated', 0] + ]; + } + + /** + * Determine whether a particular dependency is installed. + * @param string $adapter Either 'gd' or 'imagick' + * @return bool + */ + protected function dependencyCheck($adapter = 'gd') + { + if ($adapter === 'gd') { + return extension_loaded('gd'); + } + + if ($adapter === 'imagick') { + return class_exists('\Imagick'); + } + } + + /** + * Determine which adapter is preferred and whether or not it's available. + * Construct an instance of that adapter and return it. + * @param string $source - Source image path + * @return mixed - Either an instance of ImagickAdapter, GDAdapter or false if none of the extensions were available + */ + protected function getImageAdapter($source) + { + $imagick_exists = $this->dependencyCheck('imagick'); + $gd_exists = $this->dependencyCheck('gd'); + + if ($this->adapter === 'imagick') { + if ($imagick_exists) { + return new ImagickAdapter($source); + } else if ($gd_exists) { + return new GDAdapter($source); + } + } else if ($this->adapter === 'gd') { + if ($gd_exists) { + return new GDAdapter($source); + } else if ($imagick_exists) { + return new ImagickAdapter($source); + } + } + } + + /** + * Resizes an image using either Imagick or GD + * @param string $source - Source image path + * @param string $target - Target image path + * @param float $width - Target width + * @param float $height - Target height + * @param int [$quality=95] - Compression quality for target image + * @return bool - Returns true on success, otherwise false + */ + protected function resizeImage($source, $target, $width, $height, $quality = 95) + { + $adapter = $this->getImageAdapter($source); + $adapter->resize($width, $height); + $adapter->setQuality($quality); + + return $adapter->save($target); + } + + /** + * Called when a page is saved from the admin plugin. Will generate + * responsive image alternatives for images that don't have any. + */ + public function onAdminSave($event) + { + $page = $event['object']; + + if (!($page instanceof Page || $page instanceof PageObject)) { + return false; + } + + if (!$this->dependencyCheck('imagick') && !$this->dependencyCheck('gd')) { + $this->grav['admin']->setMessage('Neither Imagick nor GD seem to be installed. Automagic Images needs one of them to work.', 'warning'); + return; + } + + $this->sizes = (array) $this->config->get('plugins.automagic-images.sizes'); + $this->adapter = $this->config->get('plugins.automagic-images.adapter', 'imagick'); + + foreach ($page->media()->images() as $filename => $medium) { + $srcset = $medium->srcset(false); + + if ($srcset != '') { + continue; + } + + // We can't rely on the path returned from the image's own path + // method, since it points to the directory where the image is saved + // rather than where the original is stored. This means it could + // point to the global image cache directory. + $page_path = $page->path(); + $source_path = "$page_path/$filename"; + $info = pathinfo($source_path); + $count = 0; + + foreach ($this->sizes as $i => $size) { + if ($size['width'] >= $medium->width) { + continue; + } + + $count++; + $basename = str_replace(" ", "-", $info['filename']); + $ext = strtolower($info['extension']); + $dest_path = "{$info['dirname']}/{$basename}@{$count}x.{$ext}"; + $width = $size['width']; + $quality = $size['quality']; + $height = ($width / $medium->width) * $medium->height; + + $this->resizeImage($source_path, $dest_path, $width, $height, $quality, $medium->width, $medium->height); + } + + $remove_original = $this->config->get('plugins.automagic-images.remove_original'); + + if ($count > 0) { + $original_index = $count + 1; + + if ($remove_original) { + unlink($source_path); + } else { + rename($source_path, "{$info['dirname']}/{$basename}@{$original_index}x.{$ext}"); + } + + $fixed_source = str_replace($info['filename'], $basename, $source_path); + $fixed_source = str_replace($info['extension'], $ext, $fixed_source); + rename("{$info['dirname']}/{$basename}@1x.{$ext}", $fixed_source); + } + + $message = "Resized $filename $count times"; + + if ($remove_original) { + $message .= ' (and removed the original image)'; + } + + $this->grav['admin']->setMessage($message, 'info'); + } + } + + /** + * Iterates over images in page content that was generated via twig and adds + * sizes attribute (not cacheable) + * + * @return void + */ + public function onOutputGenerated() + { + if ($this->isAdmin()) { + return; + } + $config = (array) $this->config->get('plugins.automagic-images'); + $page = $this->grav['page']; + // dump($this->grav->output); exit; + $config = $this->mergeConfig($page); + if ($config['enabled']) { + include __DIR__ . '/vendor/autoload.php'; + $dom = new Dom; + $dom->loadStr($this->grav->output, + (new Options())->setCleanupInput(false) + ); + $images = $dom->find('img'); + $arrClasses = []; + foreach ($config['sizesattr'] as $array) { + $arrClasses[$array['class']] = $array['directive']; + } + foreach ($images as $image) { + $sizesattr = ""; + $classes = explode(" ", $image->getAttribute('class')); + foreach ($classes as $class) { + if (array_key_exists($class, $arrClasses)) { + $sizesattr = $arrClasses[$class]; + } + } + if ($sizesattr == "") { + if (array_key_exists('default', $arrClasses)) { + $sizesattr = $arrClasses['default']; + } + } + if ($sizesattr != "") { + $image->setAttribute('sizes', $sizesattr); + } + } + $this->grav->output = $dom->outerHtml; + } + } +} diff --git a/user/plugins/automagic-images/automagic-images.yaml b/user/plugins/automagic-images/automagic-images.yaml new file mode 100644 index 0000000..42847ae --- /dev/null +++ b/user/plugins/automagic-images/automagic-images.yaml @@ -0,0 +1,27 @@ +enabled: '1' +adapter: imagick +remove_original: '0' +sizes: + - + width: 640 + quality: 92 + - + width: 1000 + quality: 90 + - + width: 1500 + quality: 87 + - + width: 2500 + quality: 85 + - + width: 3500 + quality: 82 + +sizesattr: + - + class: 'default' + directive: '100vw' + - + class: 'example' + directive: '(min-width: 20em) 33%, 100vw' \ No newline at end of file diff --git a/user/plugins/automagic-images/blueprints.yaml b/user/plugins/automagic-images/blueprints.yaml new file mode 100644 index 0000000..754eed4 --- /dev/null +++ b/user/plugins/automagic-images/blueprints.yaml @@ -0,0 +1,76 @@ +name: Automagic Images +version: 1.0.5 +description: Generate responsive versions of images as they are uploaded, and add sizes directives per CSS class. +icon: picture-o +author: + name: Netzhexe + email: ak@netzhexe.de +homepage: https://github.com/skinofthesoul/grav-plugin-automagic-images +keywords: images, responsive, srcset, sizes +bugs: https://github.com/skinofthesoul/grav-plugin-automagic-images/issues +docs: https://github.com/skinofthesoul/grav-plugin-automagic-images/blob/master/README.md +license: MIT + +form: + validation: strict + fields: + enabled: + type: toggle + label: PLUGIN_ADMIN.PLUGIN_STATUS + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + adapter: + type: select + label: PLUGIN_AUTOMAGIC_IMAGES.LABEL_ADAPTER + help: PLUGIN_AUTOMAGIC_IMAGES.HELP_ADAPTER + default: imagick + options: + imagick: Imagick + gd: GD + remove_original: + type: toggle + label: PLUGIN_AUTOMAGIC_IMAGES.LABEL_REMOVE_ORIGINAL + help: PLUGIN_AUTOMAGIC_IMAGES.HELP_REMOVE_ORIGINAL + highlight: 0 + default: 0 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + sizes: + type: list + label: PLUGIN_AUTOMAGIC_IMAGES.LABEL_SIZES + btnLabel: PLUGIN_AUTOMAGIC_IMAGES.BTNLABEL_ADD_SIZE + + fields: + .width: + type: number + label: PLUGIN_AUTOMAGIC_IMAGES.LABEL_WIDTH + placeholder: 1280 + min: 1 + .quality: + type: number + label: PLUGIN_AUTOMAGIC_IMAGES.LABEL_QUALITY + placeholder: 82 + min: 0 + max: 100 + default: 82 + + sizesattr: + type: list + label: PLUGIN_AUTOMAGIC_IMAGES.LABEL_SIZESATTR + btnLabel: PLUGIN_AUTOMAGIC_IMAGES.BTNLABEL_SIZESATTR + + fields: + .class: + type: text + label: PLUGIN_AUTOMAGIC_IMAGES.LABEL_CLASS + .directive: + type: text + label: PLUGIN_AUTOMAGIC_IMAGES.LABEL_DIRECTIVE diff --git a/user/plugins/automagic-images/composer.json b/user/plugins/automagic-images/composer.json new file mode 100644 index 0000000..35a3307 --- /dev/null +++ b/user/plugins/automagic-images/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "paquettg/php-html-parser": "^3.1.1" + } +} diff --git a/user/plugins/automagic-images/composer.lock b/user/plugins/automagic-images/composer.lock new file mode 100644 index 0000000..9b3f4e2 --- /dev/null +++ b/user/plugins/automagic-images/composer.lock @@ -0,0 +1,698 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "69fdec62fcc07c25d68a76eaed5f7c8f", + "packages": [ + { + "name": "guzzlehttp/guzzle", + "version": "7.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7008573787b430c1c1f650e3722d9bba59967628" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7008573787b430c1c1f650e3722d9bba59967628", + "reference": "7008573787b430c1c1f650e3722d9bba59967628", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.4", + "guzzlehttp/psr7": "^1.7 || ^2.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-curl": "*", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "psr/log": "^1.1" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.3-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://github.com/alexeyshockov", + "type": "github" + }, + { + "url": "https://github.com/gmponos", + "type": "github" + } + ], + "time": "2021-03-23T11:33:13+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.4.1" + }, + "time": "2021-03-07T09:25:29+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.8.2" + }, + "time": "2021-04-26T09:17:50+00:00" + }, + { + "name": "myclabs/php-enum", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "46cf3d8498b095bd33727b13fd5707263af99421" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/46cf3d8498b095bd33727b13fd5707263af99421", + "reference": "46cf3d8498b095bd33727b13fd5707263af99421", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.5.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "http://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "time": "2021-02-15T16:11:48+00:00" + }, + { + "name": "paquettg/php-html-parser", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/paquettg/php-html-parser.git", + "reference": "4e01a438ad5961cc2d7427eb9798d213c8a12629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paquettg/php-html-parser/zipball/4e01a438ad5961cc2d7427eb9798d213c8a12629", + "reference": "4e01a438ad5961cc2d7427eb9798d213c8a12629", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-mbstring": "*", + "ext-zlib": "*", + "guzzlehttp/guzzle": "^7.0", + "guzzlehttp/psr7": "^1.6", + "myclabs/php-enum": "^1.7", + "paquettg/string-encode": "~1.0.0", + "php": ">=7.2", + "php-http/httplug": "^2.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "infection/infection": "^0.13.4", + "mockery/mockery": "^1.2", + "phan/phan": "^2.4", + "phpunit/phpunit": "^7.5.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPHtmlParser\\": "src/PHPHtmlParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gilles Paquette", + "email": "paquettg@gmail.com", + "homepage": "http://gillespaquette.ca" + } + ], + "description": "An HTML DOM parser. It allows you to manipulate HTML. Find tags on an HTML page with selectors just like jQuery.", + "homepage": "https://github.com/paquettg/php-html-parser", + "keywords": [ + "dom", + "html", + "parser" + ], + "support": { + "issues": "https://github.com/paquettg/php-html-parser/issues", + "source": "https://github.com/paquettg/php-html-parser/tree/3.1.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/paquettg/php-html-parser", + "type": "tidelift" + } + ], + "time": "2020-11-01T20:34:43+00:00" + }, + { + "name": "paquettg/string-encode", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/paquettg/string-encoder.git", + "reference": "a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paquettg/string-encoder/zipball/a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee", + "reference": "a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5.1" + }, + "type": "library", + "autoload": { + "psr-0": { + "stringEncode": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gilles Paquette", + "email": "paquettg@gmail.com", + "homepage": "http://gillespaquette.ca" + } + ], + "description": "Facilitating the process of altering string encoding in PHP.", + "homepage": "https://github.com/paquettg/string-encoder", + "keywords": [ + "charset", + "encoding", + "string" + ], + "support": { + "issues": "https://github.com/paquettg/string-encoder/issues", + "source": "https://github.com/paquettg/string-encoder/tree/1.0.1" + }, + "time": "2018-12-21T02:25:09+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "191a0a1b41ed026b717421931f8d3bd2514ffbf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/191a0a1b41ed026b717421931f8d3bd2514ffbf9", + "reference": "191a0a1b41ed026b717421931f8d3bd2514ffbf9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1", + "phpspec/phpspec": "^5.1 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/master" + }, + "time": "2020-07-13T15:43:23+00:00" + }, + { + "name": "php-http/promise", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", + "phpspec/phpspec": "^5.1.2 || ^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.1.0" + }, + "time": "2020-07-07T09:29:14+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/user/plugins/automagic-images/composer.phar b/user/plugins/automagic-images/composer.phar new file mode 100644 index 0000000000000000000000000000000000000000..76d6ceb2772aa232fde7e7d4bb76abd471bdd1bb GIT binary patch literal 2216568 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⁢O{3oG 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 %T*7*ef&QVM|;%yR~+_O)PkX> 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

    VK1v@*I~iBBBhQ zC3OB8TN2?FG%coxAu<_Yhb`O^0<{!M^t%K6HMQ?$Oo(U|VKiG#R#`TQx^Siy`A_9K zmO+;(zC1H0Tb8g}u|s!&J5jM0g~JZoIZh`&u0|^899TB57IDn(`1I|llYqpw=Ed2o z3Buq2rlS~Raa0#w2Mcxyoufr;e}(8KVG&u9d&p8z(u5pF^vRT2RKk7HC_2N1DQAd? z%bsM)4Qb(`yajhnuJ&eCtO%kM@28DmB!a1KTY}V+W_Cd7-0#@Il3*X@;*)9l7IZ|i z(p+Y$RwT=qWgY`inSCoc)^v=uTu7~Eoy$xc4eQCe$7wljSXNQwD+St7Qm9)oL5BvS zkcEPVt#nzOw`iW0KJR4hQXOTxqzHs?Yo|kOhRCs65_Iqb<|iDT2|ZqcN0~% zQe~kFf6DDT&ogCeTKE|6EPAeRh?CO3rv zB~dk%t}G-mPlvX z2I$#%`WWsJbVbiW;?M{s##iU@j6ikGP8ee8=!psGH>;PmdvU`kYP< z5N)PC9+Gpx6$hOu2K6--0#NkBuy|#merFr=gw^6}U)l1dST4|Ga?gWXUNB(x-glN3 z5eSQhb`=1!rQqHTTtXw&%rFygN$2KE?^|yMMfk2x4IBbhHb!WI-1+eom?i53@TDvc zPEYu8Hj$6v1rzD>iV>dv;^#g4{K`IcMR9ukJNx~6`}~7_{?R`F#LuI%bAf|TfrC$h zgHM5jPl1C^frC$hgHM5j&p%0^>K@Z1e2btpO~u%~4d}3_Lvm}9k1H{9);O#_SZn8w zp8ZW|;Axc^#h911!^q9DR4;zyhixf4Ic1}_m7I{UQnX=pOou1$VLvf?JRFMzl^|jc zNyZ2vR!L}o<}@BiiU{=-YoDu#1wK!?G9qJki@;hGH!ECR$R#lXNctQg9bnP3(d6~; zYzo_(&xV~PChdds;gAy~v)v#$VjG_HC%=#rs{u<6jj574H3QO7IMaYdT;wsD?DL8m zY)JD=JuIM*m5>p9-F|~3I7}I$^X3{Moxx8++jtTZ%7sVI{`F#^CmmS$**^6{U&%pS ztd;H#245`=2KNR7va%={YX7`}6r6Wy_=J=0fNMt{ugxZb8*Yx88)Hj7Nhyp_H<<d@9XjFo4 z3tb$L3`TT0c$g&gUJNa58JWvsO%o)dKt-a7nQ>VsF$rjdbsG-q9Kw=M&7Mp7A?vrV zo}p-_NZunea?J$3q$gJ%e)TW}XtPB3GQwD3+)%vjvqexzmkLEjgk5i3UqV* z4z<8F$}C0Fw>nFY-5Wj2-B0^=4?Y6!n*B+F{&*+ zk%UpoU#gYp{PD+zr?!5{-jbWh9Bp0U+VSazts$Z7j_`F@Z`K4?=RIK? zlkX&MewM&n={E9ynsq_P))hkzqa-AjVw~76oeq+2%3#}87LJqezlOd^R)GxOhl$md zAbuPIv&Fp%Q8M+FP0rhjOdNH)2>EikQ>)2#0b~Kz)kYb5i^(>7^ZJzgt}}+MSf2$@ z;27&l65nGuw$kZZ>9}D~a8bdnbsoKzB=gkhf?-U4t+oIbkAAY=q|y% z4Et|Hlg4Lokfn{*Gf`(nHZ(Y5@bKaoR{KEU>GuMb7kn(a0JNZfZz=erx|nC?jMzB3 zcr`vjz6gGV`~?L->BaY0)!|eXKKQdJQAH99&Fg2VZAKG;BZ7%W3W273&T{%Ohy zl5#Ww^9IWpnGW{fJknbUe8n@*#UKVMAD&nRZm`z?9Ex+s#^n4%@mSq}X~6>_e){R70+%Gn zu;E7;s&b*1rPL_sDLKexsBvwJYw=iMb$0-EXsyJ5Y3 zG%=<38

    OMOebR5S4SQ=#JS6SD`x0-&L+#BMdgaJ92^ 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�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 z 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 z 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%0A$E%;}1ZFoHqUd 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 + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + private $vendorDir; + + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + private static $registeredLoaders = array(); + + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + } + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders indexed by their corresponding vendor directories. + * + * @return self[] + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/user/plugins/automagic-images/vendor/composer/InstalledVersions.php b/user/plugins/automagic-images/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..86efd35 --- /dev/null +++ b/user/plugins/automagic-images/vendor/composer/InstalledVersions.php @@ -0,0 +1,398 @@ + + array ( + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'aliases' => + array ( + ), + 'reference' => NULL, + 'name' => '__root__', + ), + 'versions' => + array ( + '__root__' => + array ( + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'aliases' => + array ( + ), + 'reference' => NULL, + ), + 'guzzlehttp/guzzle' => + array ( + 'pretty_version' => '7.3.0', + 'version' => '7.3.0.0', + 'aliases' => + array ( + ), + 'reference' => '7008573787b430c1c1f650e3722d9bba59967628', + ), + 'guzzlehttp/promises' => + array ( + 'pretty_version' => '1.4.1', + 'version' => '1.4.1.0', + 'aliases' => + array ( + ), + 'reference' => '8e7d04f1f6450fef59366c399cfad4b9383aa30d', + ), + 'guzzlehttp/psr7' => + array ( + 'pretty_version' => '1.8.2', + 'version' => '1.8.2.0', + 'aliases' => + array ( + ), + 'reference' => 'dc960a912984efb74d0a90222870c72c87f10c91', + ), + 'myclabs/php-enum' => + array ( + 'pretty_version' => '1.8.0', + 'version' => '1.8.0.0', + 'aliases' => + array ( + ), + 'reference' => '46cf3d8498b095bd33727b13fd5707263af99421', + ), + 'paquettg/php-html-parser' => + array ( + 'pretty_version' => '3.1.1', + 'version' => '3.1.1.0', + 'aliases' => + array ( + ), + 'reference' => '4e01a438ad5961cc2d7427eb9798d213c8a12629', + ), + 'paquettg/string-encode' => + array ( + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'aliases' => + array ( + ), + 'reference' => 'a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee', + ), + 'php-http/httplug' => + array ( + 'pretty_version' => '2.2.0', + 'version' => '2.2.0.0', + 'aliases' => + array ( + ), + 'reference' => '191a0a1b41ed026b717421931f8d3bd2514ffbf9', + ), + 'php-http/promise' => + array ( + 'pretty_version' => '1.1.0', + 'version' => '1.1.0.0', + 'aliases' => + array ( + ), + 'reference' => '4c4c1f9b7289a2ec57cde7f1e9762a5789506f88', + ), + 'psr/http-client' => + array ( + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'aliases' => + array ( + ), + 'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621', + ), + 'psr/http-client-implementation' => + array ( + 'provided' => + array ( + 0 => '1.0', + ), + ), + 'psr/http-message' => + array ( + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'aliases' => + array ( + ), + 'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363', + ), + 'psr/http-message-implementation' => + array ( + 'provided' => + array ( + 0 => '1.0', + ), + ), + 'ralouphie/getallheaders' => + array ( + 'pretty_version' => '3.0.3', + 'version' => '3.0.3.0', + 'aliases' => + array ( + ), + 'reference' => '120b605dfeb996808c31b6477290a714d356e822', + ), + ), +); +private static $canGetVendors; +private static $installedByVendor = array(); + + + + + + + +public static function getInstalledPackages() +{ +$packages = array(); +foreach (self::getInstalled() as $installed) { +$packages[] = array_keys($installed['versions']); +} + +if (1 === \count($packages)) { +return $packages[0]; +} + +return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); +} + + + + + + + + + +public static function isInstalled($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (isset($installed['versions'][$packageName])) { +return true; +} +} + +return false; +} + + + + + + + + + + + + + + +public static function satisfies(VersionParser $parser, $packageName, $constraint) +{ +$constraint = $parser->parseConstraints($constraint); +$provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + +return $provided->matches($constraint); +} + + + + + + + + + + +public static function getVersionRanges($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +$ranges = array(); +if (isset($installed['versions'][$packageName]['pretty_version'])) { +$ranges[] = $installed['versions'][$packageName]['pretty_version']; +} +if (array_key_exists('aliases', $installed['versions'][$packageName])) { +$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); +} +if (array_key_exists('replaced', $installed['versions'][$packageName])) { +$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); +} +if (array_key_exists('provided', $installed['versions'][$packageName])) { +$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); +} + +return implode(' || ', $ranges); +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getVersion($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +if (!isset($installed['versions'][$packageName]['version'])) { +return null; +} + +return $installed['versions'][$packageName]['version']; +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getPrettyVersion($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +if (!isset($installed['versions'][$packageName]['pretty_version'])) { +return null; +} + +return $installed['versions'][$packageName]['pretty_version']; +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getReference($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +if (!isset($installed['versions'][$packageName]['reference'])) { +return null; +} + +return $installed['versions'][$packageName]['reference']; +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getRootPackage() +{ +$installed = self::getInstalled(); + +return $installed[0]['root']; +} + + + + + + + +public static function getRawData() +{ +return self::$installed; +} + + + + + + + + + + + + + + + + + + + +public static function reload($data) +{ +self::$installed = $data; +self::$installedByVendor = array(); +} + + + + + +private static function getInstalled() +{ +if (null === self::$canGetVendors) { +self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); +} + +$installed = array(); + +if (self::$canGetVendors) { +foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { +if (isset(self::$installedByVendor[$vendorDir])) { +$installed[] = self::$installedByVendor[$vendorDir]; +} elseif (is_file($vendorDir.'/composer/installed.php')) { +$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; +} +} +} + +$installed[] = self::$installed; + +return $installed; +} +} diff --git a/user/plugins/automagic-images/vendor/composer/LICENSE b/user/plugins/automagic-images/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/user/plugins/automagic-images/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/user/plugins/automagic-images/vendor/composer/autoload_classmap.php b/user/plugins/automagic-images/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..b26f1b1 --- /dev/null +++ b/user/plugins/automagic-images/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/user/plugins/automagic-images/vendor/composer/autoload_files.php b/user/plugins/automagic-images/vendor/composer/autoload_files.php new file mode 100644 index 0000000..c59eab5 --- /dev/null +++ b/user/plugins/automagic-images/vendor/composer/autoload_files.php @@ -0,0 +1,13 @@ + $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', + 'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php', + 'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php', + '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', +); diff --git a/user/plugins/automagic-images/vendor/composer/autoload_namespaces.php b/user/plugins/automagic-images/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..5c28f75 --- /dev/null +++ b/user/plugins/automagic-images/vendor/composer/autoload_namespaces.php @@ -0,0 +1,10 @@ + array($vendorDir . '/paquettg/string-encode/src'), +); diff --git a/user/plugins/automagic-images/vendor/composer/autoload_psr4.php b/user/plugins/automagic-images/vendor/composer/autoload_psr4.php new file mode 100644 index 0000000..1116c6f --- /dev/null +++ b/user/plugins/automagic-images/vendor/composer/autoload_psr4.php @@ -0,0 +1,18 @@ + array($vendorDir . '/psr/http-message/src'), + 'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'), + 'PHPHtmlParser\\' => array($vendorDir . '/paquettg/php-html-parser/src/PHPHtmlParser'), + 'MyCLabs\\Enum\\' => array($vendorDir . '/myclabs/php-enum/src'), + 'Http\\Promise\\' => array($vendorDir . '/php-http/promise/src'), + 'Http\\Client\\' => array($vendorDir . '/php-http/httplug/src'), + 'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'), + 'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'), + 'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'), +); diff --git a/user/plugins/automagic-images/vendor/composer/autoload_real.php b/user/plugins/automagic-images/vendor/composer/autoload_real.php new file mode 100644 index 0000000..455d254 --- /dev/null +++ b/user/plugins/automagic-images/vendor/composer/autoload_real.php @@ -0,0 +1,75 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInitec63ac3db7bdf9f71afc9dc86daedde7::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + if ($useStaticLoader) { + $includeFiles = Composer\Autoload\ComposerStaticInitec63ac3db7bdf9f71afc9dc86daedde7::$files; + } else { + $includeFiles = require __DIR__ . '/autoload_files.php'; + } + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequireec63ac3db7bdf9f71afc9dc86daedde7($fileIdentifier, $file); + } + + return $loader; + } +} + +function composerRequireec63ac3db7bdf9f71afc9dc86daedde7($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + } +} diff --git a/user/plugins/automagic-images/vendor/composer/autoload_static.php b/user/plugins/automagic-images/vendor/composer/autoload_static.php new file mode 100644 index 0000000..72648d5 --- /dev/null +++ b/user/plugins/automagic-images/vendor/composer/autoload_static.php @@ -0,0 +1,103 @@ + __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', + 'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php', + 'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php', + '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'P' => + array ( + 'Psr\\Http\\Message\\' => 17, + 'Psr\\Http\\Client\\' => 16, + 'PHPHtmlParser\\' => 14, + ), + 'M' => + array ( + 'MyCLabs\\Enum\\' => 13, + ), + 'H' => + array ( + 'Http\\Promise\\' => 13, + 'Http\\Client\\' => 12, + ), + 'G' => + array ( + 'GuzzleHttp\\Psr7\\' => 16, + 'GuzzleHttp\\Promise\\' => 19, + 'GuzzleHttp\\' => 11, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Psr\\Http\\Message\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/http-message/src', + ), + 'Psr\\Http\\Client\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/http-client/src', + ), + 'PHPHtmlParser\\' => + array ( + 0 => __DIR__ . '/..' . '/paquettg/php-html-parser/src/PHPHtmlParser', + ), + 'MyCLabs\\Enum\\' => + array ( + 0 => __DIR__ . '/..' . '/myclabs/php-enum/src', + ), + 'Http\\Promise\\' => + array ( + 0 => __DIR__ . '/..' . '/php-http/promise/src', + ), + 'Http\\Client\\' => + array ( + 0 => __DIR__ . '/..' . '/php-http/httplug/src', + ), + 'GuzzleHttp\\Psr7\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src', + ), + 'GuzzleHttp\\Promise\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/promises/src', + ), + 'GuzzleHttp\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src', + ), + ); + + public static $prefixesPsr0 = array ( + 's' => + array ( + 'stringEncode' => + array ( + 0 => __DIR__ . '/..' . '/paquettg/string-encode/src', + ), + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitec63ac3db7bdf9f71afc9dc86daedde7::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitec63ac3db7bdf9f71afc9dc86daedde7::$prefixDirsPsr4; + $loader->prefixesPsr0 = ComposerStaticInitec63ac3db7bdf9f71afc9dc86daedde7::$prefixesPsr0; + $loader->classMap = ComposerStaticInitec63ac3db7bdf9f71afc9dc86daedde7::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/user/plugins/automagic-images/vendor/composer/installed.json b/user/plugins/automagic-images/vendor/composer/installed.json new file mode 100644 index 0000000..8db51e8 --- /dev/null +++ b/user/plugins/automagic-images/vendor/composer/installed.json @@ -0,0 +1,718 @@ +{ + "packages": [ + { + "name": "guzzlehttp/guzzle", + "version": "7.3.0", + "version_normalized": "7.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7008573787b430c1c1f650e3722d9bba59967628" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7008573787b430c1c1f650e3722d9bba59967628", + "reference": "7008573787b430c1c1f650e3722d9bba59967628", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.4", + "guzzlehttp/psr7": "^1.7 || ^2.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-curl": "*", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "psr/log": "^1.1" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "time": "2021-03-23T11:33:13+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.3-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://github.com/alexeyshockov", + "type": "github" + }, + { + "url": "https://github.com/gmponos", + "type": "github" + } + ], + "install-path": "../guzzlehttp/guzzle" + }, + { + "name": "guzzlehttp/promises", + "version": "1.4.1", + "version_normalized": "1.4.1.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "time": "2021-03-07T09:25:29+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.4.1" + }, + "install-path": "../guzzlehttp/promises" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.8.2", + "version_normalized": "1.8.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "time": "2021-04-26T09:17:50+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.8.2" + }, + "install-path": "../guzzlehttp/psr7" + }, + { + "name": "myclabs/php-enum", + "version": "1.8.0", + "version_normalized": "1.8.0.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "46cf3d8498b095bd33727b13fd5707263af99421" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/46cf3d8498b095bd33727b13fd5707263af99421", + "reference": "46cf3d8498b095bd33727b13fd5707263af99421", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.5.1" + }, + "time": "2021-02-15T16:11:48+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "http://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "install-path": "../myclabs/php-enum" + }, + { + "name": "paquettg/php-html-parser", + "version": "3.1.1", + "version_normalized": "3.1.1.0", + "source": { + "type": "git", + "url": "https://github.com/paquettg/php-html-parser.git", + "reference": "4e01a438ad5961cc2d7427eb9798d213c8a12629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paquettg/php-html-parser/zipball/4e01a438ad5961cc2d7427eb9798d213c8a12629", + "reference": "4e01a438ad5961cc2d7427eb9798d213c8a12629", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-mbstring": "*", + "ext-zlib": "*", + "guzzlehttp/guzzle": "^7.0", + "guzzlehttp/psr7": "^1.6", + "myclabs/php-enum": "^1.7", + "paquettg/string-encode": "~1.0.0", + "php": ">=7.2", + "php-http/httplug": "^2.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "infection/infection": "^0.13.4", + "mockery/mockery": "^1.2", + "phan/phan": "^2.4", + "phpunit/phpunit": "^7.5.1" + }, + "time": "2020-11-01T20:34:43+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "PHPHtmlParser\\": "src/PHPHtmlParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gilles Paquette", + "email": "paquettg@gmail.com", + "homepage": "http://gillespaquette.ca" + } + ], + "description": "An HTML DOM parser. It allows you to manipulate HTML. Find tags on an HTML page with selectors just like jQuery.", + "homepage": "https://github.com/paquettg/php-html-parser", + "keywords": [ + "dom", + "html", + "parser" + ], + "support": { + "issues": "https://github.com/paquettg/php-html-parser/issues", + "source": "https://github.com/paquettg/php-html-parser/tree/3.1.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/paquettg/php-html-parser", + "type": "tidelift" + } + ], + "install-path": "../paquettg/php-html-parser" + }, + { + "name": "paquettg/string-encode", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/paquettg/string-encoder.git", + "reference": "a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paquettg/string-encoder/zipball/a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee", + "reference": "a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5.1" + }, + "time": "2018-12-21T02:25:09+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "stringEncode": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gilles Paquette", + "email": "paquettg@gmail.com", + "homepage": "http://gillespaquette.ca" + } + ], + "description": "Facilitating the process of altering string encoding in PHP.", + "homepage": "https://github.com/paquettg/string-encoder", + "keywords": [ + "charset", + "encoding", + "string" + ], + "support": { + "issues": "https://github.com/paquettg/string-encoder/issues", + "source": "https://github.com/paquettg/string-encoder/tree/1.0.1" + }, + "install-path": "../paquettg/string-encode" + }, + { + "name": "php-http/httplug", + "version": "2.2.0", + "version_normalized": "2.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "191a0a1b41ed026b717421931f8d3bd2514ffbf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/191a0a1b41ed026b717421931f8d3bd2514ffbf9", + "reference": "191a0a1b41ed026b717421931f8d3bd2514ffbf9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1", + "phpspec/phpspec": "^5.1 || ^6.0" + }, + "time": "2020-07-13T15:43:23+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/master" + }, + "install-path": "../php-http/httplug" + }, + { + "name": "php-http/promise", + "version": "1.1.0", + "version_normalized": "1.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", + "phpspec/phpspec": "^5.1.2 || ^6.2" + }, + "time": "2020-07-07T09:29:14+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.1.0" + }, + "install-path": "../php-http/promise" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "time": "2020-06-29T06:28:15+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "install-path": "../psr/http-client" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2016-08-06T14:39:51+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "install-path": "../psr/http-message" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "version_normalized": "3.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "time": "2019-03-08T08:55:37+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "install-path": "../ralouphie/getallheaders" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/user/plugins/automagic-images/vendor/composer/installed.php b/user/plugins/automagic-images/vendor/composer/installed.php new file mode 100644 index 0000000..023bb6b --- /dev/null +++ b/user/plugins/automagic-images/vendor/composer/installed.php @@ -0,0 +1,137 @@ + + array ( + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'aliases' => + array ( + ), + 'reference' => NULL, + 'name' => '__root__', + ), + 'versions' => + array ( + '__root__' => + array ( + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'aliases' => + array ( + ), + 'reference' => NULL, + ), + 'guzzlehttp/guzzle' => + array ( + 'pretty_version' => '7.3.0', + 'version' => '7.3.0.0', + 'aliases' => + array ( + ), + 'reference' => '7008573787b430c1c1f650e3722d9bba59967628', + ), + 'guzzlehttp/promises' => + array ( + 'pretty_version' => '1.4.1', + 'version' => '1.4.1.0', + 'aliases' => + array ( + ), + 'reference' => '8e7d04f1f6450fef59366c399cfad4b9383aa30d', + ), + 'guzzlehttp/psr7' => + array ( + 'pretty_version' => '1.8.2', + 'version' => '1.8.2.0', + 'aliases' => + array ( + ), + 'reference' => 'dc960a912984efb74d0a90222870c72c87f10c91', + ), + 'myclabs/php-enum' => + array ( + 'pretty_version' => '1.8.0', + 'version' => '1.8.0.0', + 'aliases' => + array ( + ), + 'reference' => '46cf3d8498b095bd33727b13fd5707263af99421', + ), + 'paquettg/php-html-parser' => + array ( + 'pretty_version' => '3.1.1', + 'version' => '3.1.1.0', + 'aliases' => + array ( + ), + 'reference' => '4e01a438ad5961cc2d7427eb9798d213c8a12629', + ), + 'paquettg/string-encode' => + array ( + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'aliases' => + array ( + ), + 'reference' => 'a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee', + ), + 'php-http/httplug' => + array ( + 'pretty_version' => '2.2.0', + 'version' => '2.2.0.0', + 'aliases' => + array ( + ), + 'reference' => '191a0a1b41ed026b717421931f8d3bd2514ffbf9', + ), + 'php-http/promise' => + array ( + 'pretty_version' => '1.1.0', + 'version' => '1.1.0.0', + 'aliases' => + array ( + ), + 'reference' => '4c4c1f9b7289a2ec57cde7f1e9762a5789506f88', + ), + 'psr/http-client' => + array ( + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'aliases' => + array ( + ), + 'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621', + ), + 'psr/http-client-implementation' => + array ( + 'provided' => + array ( + 0 => '1.0', + ), + ), + 'psr/http-message' => + array ( + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'aliases' => + array ( + ), + 'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363', + ), + 'psr/http-message-implementation' => + array ( + 'provided' => + array ( + 0 => '1.0', + ), + ), + 'ralouphie/getallheaders' => + array ( + 'pretty_version' => '3.0.3', + 'version' => '3.0.3.0', + 'aliases' => + array ( + ), + 'reference' => '120b605dfeb996808c31b6477290a714d356e822', + ), + ), +); diff --git a/user/plugins/automagic-images/vendor/composer/platform_check.php b/user/plugins/automagic-images/vendor/composer/platform_check.php new file mode 100644 index 0000000..92370c5 --- /dev/null +++ b/user/plugins/automagic-images/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 70300)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/CHANGELOG.md b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/CHANGELOG.md new file mode 100644 index 0000000..e303af2 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/CHANGELOG.md @@ -0,0 +1,1462 @@ +# Change Log + +Please refer to [UPGRADING](UPGRADING.md) guide for upgrading to a major version. + +## 7.3.0 - 2021-03-23 + +### Added + +- Support for DER and P12 certificates [#2413](https://github.com/guzzle/guzzle/pull/2413) +- Support the cURL (http://) scheme for StreamHandler proxies [#2850](https://github.com/guzzle/guzzle/pull/2850) +- Support for `guzzlehttp/psr7:^2.0` [#2878](https://github.com/guzzle/guzzle/pull/2878) + +### Fixed + +- Handle exceptions on invalid header consistently between PHP versions and handlers [#2872](https://github.com/guzzle/guzzle/pull/2872) + +## 7.2.0 - 2020-10-10 + +### Added + +- Support for PHP 8 [#2712](https://github.com/guzzle/guzzle/pull/2712), [#2715](https://github.com/guzzle/guzzle/pull/2715), [#2789](https://github.com/guzzle/guzzle/pull/2789) +- Support passing a body summarizer to the http errors middleware [#2795](https://github.com/guzzle/guzzle/pull/2795) + +### Fixed + +- Handle exceptions during response creation [#2591](https://github.com/guzzle/guzzle/pull/2591) +- Fix CURLOPT_ENCODING not to be overwritten [#2595](https://github.com/guzzle/guzzle/pull/2595) +- Make sure the Request always has a body object [#2804](https://github.com/guzzle/guzzle/pull/2804) + +### Changed + +- The `TooManyRedirectsException` has a response [#2660](https://github.com/guzzle/guzzle/pull/2660) +- Avoid "functions" from dependencies [#2712](https://github.com/guzzle/guzzle/pull/2712) + +### Deprecated + +- Using environment variable GUZZLE_CURL_SELECT_TIMEOUT [#2786](https://github.com/guzzle/guzzle/pull/2786) + +## 7.1.1 - 2020-09-30 + +### Fixed + +- Incorrect EOF detection for response body streams on Windows. + +### Changed + +- We dont connect curl `sink` on HEAD requests. +- Removed some PHP 5 workarounds + +## 7.1.0 - 2020-09-22 + +### Added + +- `GuzzleHttp\MessageFormatterInterface` + +### Fixed + +- Fixed issue that caused cookies with no value not to be stored. +- On redirects, we allow all safe methods like GET, HEAD and OPTIONS. +- Fixed logging on empty responses. +- Make sure MessageFormatter::format returns string + +### Deprecated + +- All functions in `GuzzleHttp` has been deprecated. Use static methods on `Utils` instead. +- `ClientInterface::getConfig()` +- `Client::getConfig()` +- `Client::__call()` +- `Utils::defaultCaBundle()` +- `CurlFactory::LOW_CURL_VERSION_NUMBER` + +## 7.0.1 - 2020-06-27 + +* Fix multiply defined functions fatal error [#2699](https://github.com/guzzle/guzzle/pull/2699) + +## 7.0.0 - 2020-06-27 + +No changes since 7.0.0-rc1. + +## 7.0.0-rc1 - 2020-06-15 + +### Changed + +* Use error level for logging errors in Middleware [#2629](https://github.com/guzzle/guzzle/pull/2629) +* Disabled IDN support by default and require ext-intl to use it [#2675](https://github.com/guzzle/guzzle/pull/2675) + +## 7.0.0-beta2 - 2020-05-25 + +### Added + +* Using `Utils` class instead of functions in the `GuzzleHttp` namespace. [#2546](https://github.com/guzzle/guzzle/pull/2546) +* `ClientInterface::MAJOR_VERSION` [#2583](https://github.com/guzzle/guzzle/pull/2583) + +### Changed + +* Avoid the `getenv` function when unsafe [#2531](https://github.com/guzzle/guzzle/pull/2531) +* Added real client methods [#2529](https://github.com/guzzle/guzzle/pull/2529) +* Avoid functions due to global install conflicts [#2546](https://github.com/guzzle/guzzle/pull/2546) +* Use Symfony intl-idn polyfill [#2550](https://github.com/guzzle/guzzle/pull/2550) +* Adding methods for HTTP verbs like `Client::get()`, `Client::head()`, `Client::patch()` etc [#2529](https://github.com/guzzle/guzzle/pull/2529) +* `ConnectException` extends `TransferException` [#2541](https://github.com/guzzle/guzzle/pull/2541) +* Updated the default User Agent to "GuzzleHttp/7" [#2654](https://github.com/guzzle/guzzle/pull/2654) + +### Fixed + +* Various intl icu issues [#2626](https://github.com/guzzle/guzzle/pull/2626) + +### Removed + +* Pool option `pool_size` [#2528](https://github.com/guzzle/guzzle/pull/2528) + +## 7.0.0-beta1 - 2019-12-30 + +The diff might look very big but 95% of Guzzle users will be able to upgrade without modification. +Please see [the upgrade document](UPGRADING.md) that describes all BC breaking changes. + +### Added + +* Implement PSR-18 and dropped PHP 5 support [#2421](https://github.com/guzzle/guzzle/pull/2421) [#2474](https://github.com/guzzle/guzzle/pull/2474) +* PHP 7 types [#2442](https://github.com/guzzle/guzzle/pull/2442) [#2449](https://github.com/guzzle/guzzle/pull/2449) [#2466](https://github.com/guzzle/guzzle/pull/2466) [#2497](https://github.com/guzzle/guzzle/pull/2497) [#2499](https://github.com/guzzle/guzzle/pull/2499) +* IDN support for redirects [2424](https://github.com/guzzle/guzzle/pull/2424) + +### Changed + +* Dont allow passing null as third argument to `BadResponseException::__construct()` [#2427](https://github.com/guzzle/guzzle/pull/2427) +* Use SAPI constant instead of method call [#2450](https://github.com/guzzle/guzzle/pull/2450) +* Use native function invocation [#2444](https://github.com/guzzle/guzzle/pull/2444) +* Better defaults for PHP installations with old ICU lib [2454](https://github.com/guzzle/guzzle/pull/2454) +* Added visibility to all constants [#2462](https://github.com/guzzle/guzzle/pull/2462) +* Dont allow passing `null` as URI to `Client::request()` and `Client::requestAsync()` [#2461](https://github.com/guzzle/guzzle/pull/2461) +* Widen the exception argument to throwable [#2495](https://github.com/guzzle/guzzle/pull/2495) + +### Fixed + +* Logging when Promise rejected with a string [#2311](https://github.com/guzzle/guzzle/pull/2311) + +### Removed + +* Class `SeekException` [#2162](https://github.com/guzzle/guzzle/pull/2162) +* `RequestException::getResponseBodySummary()` [#2425](https://github.com/guzzle/guzzle/pull/2425) +* `CookieJar::getCookieValue()` [#2433](https://github.com/guzzle/guzzle/pull/2433) +* `uri_template()` and `UriTemplate` [#2440](https://github.com/guzzle/guzzle/pull/2440) +* Request options `save_to` and `exceptions` [#2464](https://github.com/guzzle/guzzle/pull/2464) + +## 6.5.2 - 2019-12-23 + +* idn_to_ascii() fix for old PHP versions [#2489](https://github.com/guzzle/guzzle/pull/2489) + +## 6.5.1 - 2019-12-21 + +* Better defaults for PHP installations with old ICU lib [#2454](https://github.com/guzzle/guzzle/pull/2454) +* IDN support for redirects [#2424](https://github.com/guzzle/guzzle/pull/2424) + +## 6.5.0 - 2019-12-07 + +* Improvement: Added support for reset internal queue in MockHandler. [#2143](https://github.com/guzzle/guzzle/pull/2143) +* Improvement: Added support to pass arbitrary options to `curl_multi_init`. [#2287](https://github.com/guzzle/guzzle/pull/2287) +* Fix: Gracefully handle passing `null` to the `header` option. [#2132](https://github.com/guzzle/guzzle/pull/2132) +* Fix: `RetryMiddleware` did not do exponential delay between retires due unit mismatch. [#2132](https://github.com/guzzle/guzzle/pull/2132) +* Fix: Prevent undefined offset when using array for ssl_key options. [#2348](https://github.com/guzzle/guzzle/pull/2348) +* Deprecated `ClientInterface::VERSION` + +## 6.4.1 - 2019-10-23 + +* No `guzzle.phar` was created in 6.4.0 due expired API token. This release will fix that +* Added `parent::__construct()` to `FileCookieJar` and `SessionCookieJar` + +## 6.4.0 - 2019-10-23 + +* Improvement: Improved error messages when using curl < 7.21.2 [#2108](https://github.com/guzzle/guzzle/pull/2108) +* Fix: Test if response is readable before returning a summary in `RequestException::getResponseBodySummary()` [#2081](https://github.com/guzzle/guzzle/pull/2081) +* Fix: Add support for GUZZLE_CURL_SELECT_TIMEOUT environment variable [#2161](https://github.com/guzzle/guzzle/pull/2161) +* Improvement: Added `GuzzleHttp\Exception\InvalidArgumentException` [#2163](https://github.com/guzzle/guzzle/pull/2163) +* Improvement: Added `GuzzleHttp\_current_time()` to use `hrtime()` if that function exists. [#2242](https://github.com/guzzle/guzzle/pull/2242) +* Improvement: Added curl's `appconnect_time` in `TransferStats` [#2284](https://github.com/guzzle/guzzle/pull/2284) +* Improvement: Make GuzzleException extend Throwable wherever it's available [#2273](https://github.com/guzzle/guzzle/pull/2273) +* Fix: Prevent concurrent writes to file when saving `CookieJar` [#2335](https://github.com/guzzle/guzzle/pull/2335) +* Improvement: Update `MockHandler` so we can test transfer time [#2362](https://github.com/guzzle/guzzle/pull/2362) + +## 6.3.3 - 2018-04-22 + +* Fix: Default headers when decode_content is specified + + +## 6.3.2 - 2018-03-26 + +* Fix: Release process + + +## 6.3.1 - 2018-03-26 + +* Bug fix: Parsing 0 epoch expiry times in cookies [#2014](https://github.com/guzzle/guzzle/pull/2014) +* Improvement: Better ConnectException detection [#2012](https://github.com/guzzle/guzzle/pull/2012) +* Bug fix: Malformed domain that contains a "/" [#1999](https://github.com/guzzle/guzzle/pull/1999) +* Bug fix: Undefined offset when a cookie has no first key-value pair [#1998](https://github.com/guzzle/guzzle/pull/1998) +* Improvement: Support PHPUnit 6 [#1953](https://github.com/guzzle/guzzle/pull/1953) +* Bug fix: Support empty headers [#1915](https://github.com/guzzle/guzzle/pull/1915) +* Bug fix: Ignore case during header modifications [#1916](https://github.com/guzzle/guzzle/pull/1916) + ++ Minor code cleanups, documentation fixes and clarifications. + + +## 6.3.0 - 2017-06-22 + +* Feature: force IP resolution (ipv4 or ipv6) [#1608](https://github.com/guzzle/guzzle/pull/1608), [#1659](https://github.com/guzzle/guzzle/pull/1659) +* Improvement: Don't include summary in exception message when body is empty [#1621](https://github.com/guzzle/guzzle/pull/1621) +* Improvement: Handle `on_headers` option in MockHandler [#1580](https://github.com/guzzle/guzzle/pull/1580) +* Improvement: Added SUSE Linux CA path [#1609](https://github.com/guzzle/guzzle/issues/1609) +* Improvement: Use class reference for getting the name of the class instead of using hardcoded strings [#1641](https://github.com/guzzle/guzzle/pull/1641) +* Feature: Added `read_timeout` option [#1611](https://github.com/guzzle/guzzle/pull/1611) +* Bug fix: PHP 7.x fixes [#1685](https://github.com/guzzle/guzzle/pull/1685), [#1686](https://github.com/guzzle/guzzle/pull/1686), [#1811](https://github.com/guzzle/guzzle/pull/1811) +* Deprecation: BadResponseException instantiation without a response [#1642](https://github.com/guzzle/guzzle/pull/1642) +* Feature: Added NTLM auth [#1569](https://github.com/guzzle/guzzle/pull/1569) +* Feature: Track redirect HTTP status codes [#1711](https://github.com/guzzle/guzzle/pull/1711) +* Improvement: Check handler type during construction [#1745](https://github.com/guzzle/guzzle/pull/1745) +* Improvement: Always include the Content-Length if there's a body [#1721](https://github.com/guzzle/guzzle/pull/1721) +* Feature: Added convenience method to access a cookie by name [#1318](https://github.com/guzzle/guzzle/pull/1318) +* Bug fix: Fill `CURLOPT_CAPATH` and `CURLOPT_CAINFO` properly [#1684](https://github.com/guzzle/guzzle/pull/1684) +* Improvement: Use `\GuzzleHttp\Promise\rejection_for` function instead of object init [#1827](https://github.com/guzzle/guzzle/pull/1827) + + ++ Minor code cleanups, documentation fixes and clarifications. + +## 6.2.3 - 2017-02-28 + +* Fix deprecations with guzzle/psr7 version 1.4 + +## 6.2.2 - 2016-10-08 + +* Allow to pass nullable Response to delay callable +* Only add scheme when host is present +* Fix drain case where content-length is the literal string zero +* Obfuscate in-URL credentials in exceptions + +## 6.2.1 - 2016-07-18 + +* Address HTTP_PROXY security vulnerability, CVE-2016-5385: + https://httpoxy.org/ +* Fixing timeout bug with StreamHandler: + https://github.com/guzzle/guzzle/pull/1488 +* Only read up to `Content-Length` in PHP StreamHandler to avoid timeouts when + a server does not honor `Connection: close`. +* Ignore URI fragment when sending requests. + +## 6.2.0 - 2016-03-21 + +* Feature: added `GuzzleHttp\json_encode` and `GuzzleHttp\json_decode`. + https://github.com/guzzle/guzzle/pull/1389 +* Bug fix: Fix sleep calculation when waiting for delayed requests. + https://github.com/guzzle/guzzle/pull/1324 +* Feature: More flexible history containers. + https://github.com/guzzle/guzzle/pull/1373 +* Bug fix: defer sink stream opening in StreamHandler. + https://github.com/guzzle/guzzle/pull/1377 +* Bug fix: do not attempt to escape cookie values. + https://github.com/guzzle/guzzle/pull/1406 +* Feature: report original content encoding and length on decoded responses. + https://github.com/guzzle/guzzle/pull/1409 +* Bug fix: rewind seekable request bodies before dispatching to cURL. + https://github.com/guzzle/guzzle/pull/1422 +* Bug fix: provide an empty string to `http_build_query` for HHVM workaround. + https://github.com/guzzle/guzzle/pull/1367 + +## 6.1.1 - 2015-11-22 + +* Bug fix: Proxy::wrapSync() now correctly proxies to the appropriate handler + https://github.com/guzzle/guzzle/commit/911bcbc8b434adce64e223a6d1d14e9a8f63e4e4 +* Feature: HandlerStack is now more generic. + https://github.com/guzzle/guzzle/commit/f2102941331cda544745eedd97fc8fd46e1ee33e +* Bug fix: setting verify to false in the StreamHandler now disables peer + verification. https://github.com/guzzle/guzzle/issues/1256 +* Feature: Middleware now uses an exception factory, including more error + context. https://github.com/guzzle/guzzle/pull/1282 +* Feature: better support for disabled functions. + https://github.com/guzzle/guzzle/pull/1287 +* Bug fix: fixed regression where MockHandler was not using `sink`. + https://github.com/guzzle/guzzle/pull/1292 + +## 6.1.0 - 2015-09-08 + +* Feature: Added the `on_stats` request option to provide access to transfer + statistics for requests. https://github.com/guzzle/guzzle/pull/1202 +* Feature: Added the ability to persist session cookies in CookieJars. + https://github.com/guzzle/guzzle/pull/1195 +* Feature: Some compatibility updates for Google APP Engine + https://github.com/guzzle/guzzle/pull/1216 +* Feature: Added support for NO_PROXY to prevent the use of a proxy based on + a simple set of rules. https://github.com/guzzle/guzzle/pull/1197 +* Feature: Cookies can now contain square brackets. + https://github.com/guzzle/guzzle/pull/1237 +* Bug fix: Now correctly parsing `=` inside of quotes in Cookies. + https://github.com/guzzle/guzzle/pull/1232 +* Bug fix: Cusotm cURL options now correctly override curl options of the + same name. https://github.com/guzzle/guzzle/pull/1221 +* Bug fix: Content-Type header is now added when using an explicitly provided + multipart body. https://github.com/guzzle/guzzle/pull/1218 +* Bug fix: Now ignoring Set-Cookie headers that have no name. +* Bug fix: Reason phrase is no longer cast to an int in some cases in the + cURL handler. https://github.com/guzzle/guzzle/pull/1187 +* Bug fix: Remove the Authorization header when redirecting if the Host + header changes. https://github.com/guzzle/guzzle/pull/1207 +* Bug fix: Cookie path matching fixes + https://github.com/guzzle/guzzle/issues/1129 +* Bug fix: Fixing the cURL `body_as_string` setting + https://github.com/guzzle/guzzle/pull/1201 +* Bug fix: quotes are no longer stripped when parsing cookies. + https://github.com/guzzle/guzzle/issues/1172 +* Bug fix: `form_params` and `query` now always uses the `&` separator. + https://github.com/guzzle/guzzle/pull/1163 +* Bug fix: Adding a Content-Length to PHP stream wrapper requests if not set. + https://github.com/guzzle/guzzle/pull/1189 + +## 6.0.2 - 2015-07-04 + +* Fixed a memory leak in the curl handlers in which references to callbacks + were not being removed by `curl_reset`. +* Cookies are now extracted properly before redirects. +* Cookies now allow more character ranges. +* Decoded Content-Encoding responses are now modified to correctly reflect + their state if the encoding was automatically removed by a handler. This + means that the `Content-Encoding` header may be removed an the + `Content-Length` modified to reflect the message size after removing the + encoding. +* Added a more explicit error message when trying to use `form_params` and + `multipart` in the same request. +* Several fixes for HHVM support. +* Functions are now conditionally required using an additional level of + indirection to help with global Composer installations. + +## 6.0.1 - 2015-05-27 + +* Fixed a bug with serializing the `query` request option where the `&` + separator was missing. +* Added a better error message for when `body` is provided as an array. Please + use `form_params` or `multipart` instead. +* Various doc fixes. + +## 6.0.0 - 2015-05-26 + +* See the UPGRADING.md document for more information. +* Added `multipart` and `form_params` request options. +* Added `synchronous` request option. +* Added the `on_headers` request option. +* Fixed `expect` handling. +* No longer adding default middlewares in the client ctor. These need to be + present on the provided handler in order to work. +* Requests are no longer initiated when sending async requests with the + CurlMultiHandler. This prevents unexpected recursion from requests completing + while ticking the cURL loop. +* Removed the semantics of setting `default` to `true`. This is no longer + required now that the cURL loop is not ticked for async requests. +* Added request and response logging middleware. +* No longer allowing self signed certificates when using the StreamHandler. +* Ensuring that `sink` is valid if saving to a file. +* Request exceptions now include a "handler context" which provides handler + specific contextual information. +* Added `GuzzleHttp\RequestOptions` to allow request options to be applied + using constants. +* `$maxHandles` has been removed from CurlMultiHandler. +* `MultipartPostBody` is now part of the `guzzlehttp/psr7` package. + +## 5.3.0 - 2015-05-19 + +* Mock now supports `save_to` +* Marked `AbstractRequestEvent::getTransaction()` as public. +* Fixed a bug in which multiple headers using different casing would overwrite + previous headers in the associative array. +* Added `Utils::getDefaultHandler()` +* Marked `GuzzleHttp\Client::getDefaultUserAgent` as deprecated. +* URL scheme is now always lowercased. + +## 6.0.0-beta.1 + +* Requires PHP >= 5.5 +* Updated to use PSR-7 + * Requires immutable messages, which basically means an event based system + owned by a request instance is no longer possible. + * Utilizing the [Guzzle PSR-7 package](https://github.com/guzzle/psr7). + * Removed the dependency on `guzzlehttp/streams`. These stream abstractions + are available in the `guzzlehttp/psr7` package under the `GuzzleHttp\Psr7` + namespace. +* Added middleware and handler system + * Replaced the Guzzle event and subscriber system with a middleware system. + * No longer depends on RingPHP, but rather places the HTTP handlers directly + in Guzzle, operating on PSR-7 messages. + * Retry logic is now encapsulated in `GuzzleHttp\Middleware::retry`, which + means the `guzzlehttp/retry-subscriber` is now obsolete. + * Mocking responses is now handled using `GuzzleHttp\Handler\MockHandler`. +* Asynchronous responses + * No longer supports the `future` request option to send an async request. + Instead, use one of the `*Async` methods of a client (e.g., `requestAsync`, + `getAsync`, etc.). + * Utilizing `GuzzleHttp\Promise` instead of React's promise library to avoid + recursion required by chaining and forwarding react promises. See + https://github.com/guzzle/promises + * Added `requestAsync` and `sendAsync` to send request asynchronously. + * Added magic methods for `getAsync()`, `postAsync()`, etc. to send requests + asynchronously. +* Request options + * POST and form updates + * Added the `form_fields` and `form_files` request options. + * Removed the `GuzzleHttp\Post` namespace. + * The `body` request option no longer accepts an array for POST requests. + * The `exceptions` request option has been deprecated in favor of the + `http_errors` request options. + * The `save_to` request option has been deprecated in favor of `sink` request + option. +* Clients no longer accept an array of URI template string and variables for + URI variables. You will need to expand URI templates before passing them + into a client constructor or request method. +* Client methods `get()`, `post()`, `put()`, `patch()`, `options()`, etc. are + now magic methods that will send synchronous requests. +* Replaced `Utils.php` with plain functions in `functions.php`. +* Removed `GuzzleHttp\Collection`. +* Removed `GuzzleHttp\BatchResults`. Batched pool results are now returned as + an array. +* Removed `GuzzleHttp\Query`. Query string handling is now handled using an + associative array passed into the `query` request option. The query string + is serialized using PHP's `http_build_query`. If you need more control, you + can pass the query string in as a string. +* `GuzzleHttp\QueryParser` has been replaced with the + `GuzzleHttp\Psr7\parse_query`. + +## 5.2.0 - 2015-01-27 + +* Added `AppliesHeadersInterface` to make applying headers to a request based + on the body more generic and not specific to `PostBodyInterface`. +* Reduced the number of stack frames needed to send requests. +* Nested futures are now resolved in the client rather than the RequestFsm +* Finishing state transitions is now handled in the RequestFsm rather than the + RingBridge. +* Added a guard in the Pool class to not use recursion for request retries. + +## 5.1.0 - 2014-12-19 + +* Pool class no longer uses recursion when a request is intercepted. +* The size of a Pool can now be dynamically adjusted using a callback. + See https://github.com/guzzle/guzzle/pull/943. +* Setting a request option to `null` when creating a request with a client will + ensure that the option is not set. This allows you to overwrite default + request options on a per-request basis. + See https://github.com/guzzle/guzzle/pull/937. +* Added the ability to limit which protocols are allowed for redirects by + specifying a `protocols` array in the `allow_redirects` request option. +* Nested futures due to retries are now resolved when waiting for synchronous + responses. See https://github.com/guzzle/guzzle/pull/947. +* `"0"` is now an allowed URI path. See + https://github.com/guzzle/guzzle/pull/935. +* `Query` no longer typehints on the `$query` argument in the constructor, + allowing for strings and arrays. +* Exceptions thrown in the `end` event are now correctly wrapped with Guzzle + specific exceptions if necessary. + +## 5.0.3 - 2014-11-03 + +This change updates query strings so that they are treated as un-encoded values +by default where the value represents an un-encoded value to send over the +wire. A Query object then encodes the value before sending over the wire. This +means that even value query string values (e.g., ":") are url encoded. This +makes the Query class match PHP's http_build_query function. However, if you +want to send requests over the wire using valid query string characters that do +not need to be encoded, then you can provide a string to Url::setQuery() and +pass true as the second argument to specify that the query string is a raw +string that should not be parsed or encoded (unless a call to getQuery() is +subsequently made, forcing the query-string to be converted into a Query +object). + +## 5.0.2 - 2014-10-30 + +* Added a trailing `\r\n` to multipart/form-data payloads. See + https://github.com/guzzle/guzzle/pull/871 +* Added a `GuzzleHttp\Pool::send()` convenience method to match the docs. +* Status codes are now returned as integers. See + https://github.com/guzzle/guzzle/issues/881 +* No longer overwriting an existing `application/x-www-form-urlencoded` header + when sending POST requests, allowing for customized headers. See + https://github.com/guzzle/guzzle/issues/877 +* Improved path URL serialization. + + * No longer double percent-encoding characters in the path or query string if + they are already encoded. + * Now properly encoding the supplied path to a URL object, instead of only + encoding ' ' and '?'. + * Note: This has been changed in 5.0.3 to now encode query string values by + default unless the `rawString` argument is provided when setting the query + string on a URL: Now allowing many more characters to be present in the + query string without being percent encoded. See https://tools.ietf.org/html/rfc3986#appendix-A + +## 5.0.1 - 2014-10-16 + +Bugfix release. + +* Fixed an issue where connection errors still returned response object in + error and end events event though the response is unusable. This has been + corrected so that a response is not returned in the `getResponse` method of + these events if the response did not complete. https://github.com/guzzle/guzzle/issues/867 +* Fixed an issue where transfer statistics were not being populated in the + RingBridge. https://github.com/guzzle/guzzle/issues/866 + +## 5.0.0 - 2014-10-12 + +Adding support for non-blocking responses and some minor API cleanup. + +### New Features + +* Added support for non-blocking responses based on `guzzlehttp/guzzle-ring`. +* Added a public API for creating a default HTTP adapter. +* Updated the redirect plugin to be non-blocking so that redirects are sent + concurrently. Other plugins like this can now be updated to be non-blocking. +* Added a "progress" event so that you can get upload and download progress + events. +* Added `GuzzleHttp\Pool` which implements FutureInterface and transfers + requests concurrently using a capped pool size as efficiently as possible. +* Added `hasListeners()` to EmitterInterface. +* Removed `GuzzleHttp\ClientInterface::sendAll` and marked + `GuzzleHttp\Client::sendAll` as deprecated (it's still there, just not the + recommended way). + +### Breaking changes + +The breaking changes in this release are relatively minor. The biggest thing to +look out for is that request and response objects no longer implement fluent +interfaces. + +* Removed the fluent interfaces (i.e., `return $this`) from requests, + responses, `GuzzleHttp\Collection`, `GuzzleHttp\Url`, + `GuzzleHttp\Query`, `GuzzleHttp\Post\PostBody`, and + `GuzzleHttp\Cookie\SetCookie`. This blog post provides a good outline of + why I did this: https://ocramius.github.io/blog/fluent-interfaces-are-evil/. + This also makes the Guzzle message interfaces compatible with the current + PSR-7 message proposal. +* Removed "functions.php", so that Guzzle is truly PSR-4 compliant. Except + for the HTTP request functions from function.php, these functions are now + implemented in `GuzzleHttp\Utils` using camelCase. `GuzzleHttp\json_decode` + moved to `GuzzleHttp\Utils::jsonDecode`. `GuzzleHttp\get_path` moved to + `GuzzleHttp\Utils::getPath`. `GuzzleHttp\set_path` moved to + `GuzzleHttp\Utils::setPath`. `GuzzleHttp\batch` should now be + `GuzzleHttp\Pool::batch`, which returns an `objectStorage`. Using functions.php + caused problems for many users: they aren't PSR-4 compliant, require an + explicit include, and needed an if-guard to ensure that the functions are not + declared multiple times. +* Rewrote adapter layer. + * Removing all classes from `GuzzleHttp\Adapter`, these are now + implemented as callables that are stored in `GuzzleHttp\Ring\Client`. + * Removed the concept of "parallel adapters". Sending requests serially or + concurrently is now handled using a single adapter. + * Moved `GuzzleHttp\Adapter\Transaction` to `GuzzleHttp\Transaction`. The + Transaction object now exposes the request, response, and client as public + properties. The getters and setters have been removed. +* Removed the "headers" event. This event was only useful for changing the + body a response once the headers of the response were known. You can implement + a similar behavior in a number of ways. One example might be to use a + FnStream that has access to the transaction being sent. For example, when the + first byte is written, you could check if the response headers match your + expectations, and if so, change the actual stream body that is being + written to. +* Removed the `asArray` parameter from + `GuzzleHttp\Message\MessageInterface::getHeader`. If you want to get a header + value as an array, then use the newly added `getHeaderAsArray()` method of + `MessageInterface`. This change makes the Guzzle interfaces compatible with + the PSR-7 interfaces. +* `GuzzleHttp\Message\MessageFactory` no longer allows subclasses to add + custom request options using double-dispatch (this was an implementation + detail). Instead, you should now provide an associative array to the + constructor which is a mapping of the request option name mapping to a + function that applies the option value to a request. +* Removed the concept of "throwImmediately" from exceptions and error events. + This control mechanism was used to stop a transfer of concurrent requests + from completing. This can now be handled by throwing the exception or by + cancelling a pool of requests or each outstanding future request individually. +* Updated to "GuzzleHttp\Streams" 3.0. + * `GuzzleHttp\Stream\StreamInterface::getContents()` no longer accepts a + `maxLen` parameter. This update makes the Guzzle streams project + compatible with the current PSR-7 proposal. + * `GuzzleHttp\Stream\Stream::__construct`, + `GuzzleHttp\Stream\Stream::factory`, and + `GuzzleHttp\Stream\Utils::create` no longer accept a size in the second + argument. They now accept an associative array of options, including the + "size" key and "metadata" key which can be used to provide custom metadata. + +## 4.2.2 - 2014-09-08 + +* Fixed a memory leak in the CurlAdapter when reusing cURL handles. +* No longer using `request_fulluri` in stream adapter proxies. +* Relative redirects are now based on the last response, not the first response. + +## 4.2.1 - 2014-08-19 + +* Ensuring that the StreamAdapter does not always add a Content-Type header +* Adding automated github releases with a phar and zip + +## 4.2.0 - 2014-08-17 + +* Now merging in default options using a case-insensitive comparison. + Closes https://github.com/guzzle/guzzle/issues/767 +* Added the ability to automatically decode `Content-Encoding` response bodies + using the `decode_content` request option. This is set to `true` by default + to decode the response body if it comes over the wire with a + `Content-Encoding`. Set this value to `false` to disable decoding the + response content, and pass a string to provide a request `Accept-Encoding` + header and turn on automatic response decoding. This feature now allows you + to pass an `Accept-Encoding` header in the headers of a request but still + disable automatic response decoding. + Closes https://github.com/guzzle/guzzle/issues/764 +* Added the ability to throw an exception immediately when transferring + requests in parallel. Closes https://github.com/guzzle/guzzle/issues/760 +* Updating guzzlehttp/streams dependency to ~2.1 +* No longer utilizing the now deprecated namespaced methods from the stream + package. + +## 4.1.8 - 2014-08-14 + +* Fixed an issue in the CurlFactory that caused setting the `stream=false` + request option to throw an exception. + See: https://github.com/guzzle/guzzle/issues/769 +* TransactionIterator now calls rewind on the inner iterator. + See: https://github.com/guzzle/guzzle/pull/765 +* You can now set the `Content-Type` header to `multipart/form-data` + when creating POST requests to force multipart bodies. + See https://github.com/guzzle/guzzle/issues/768 + +## 4.1.7 - 2014-08-07 + +* Fixed an error in the HistoryPlugin that caused the same request and response + to be logged multiple times when an HTTP protocol error occurs. +* Ensuring that cURL does not add a default Content-Type when no Content-Type + has been supplied by the user. This prevents the adapter layer from modifying + the request that is sent over the wire after any listeners may have already + put the request in a desired state (e.g., signed the request). +* Throwing an exception when you attempt to send requests that have the + "stream" set to true in parallel using the MultiAdapter. +* Only calling curl_multi_select when there are active cURL handles. This was + previously changed and caused performance problems on some systems due to PHP + always selecting until the maximum select timeout. +* Fixed a bug where multipart/form-data POST fields were not correctly + aggregated (e.g., values with "&"). + +## 4.1.6 - 2014-08-03 + +* Added helper methods to make it easier to represent messages as strings, + including getting the start line and getting headers as a string. + +## 4.1.5 - 2014-08-02 + +* Automatically retrying cURL "Connection died, retrying a fresh connect" + errors when possible. +* cURL implementation cleanup +* Allowing multiple event subscriber listeners to be registered per event by + passing an array of arrays of listener configuration. + +## 4.1.4 - 2014-07-22 + +* Fixed a bug that caused multi-part POST requests with more than one field to + serialize incorrectly. +* Paths can now be set to "0" +* `ResponseInterface::xml` now accepts a `libxml_options` option and added a + missing default argument that was required when parsing XML response bodies. +* A `save_to` stream is now created lazily, which means that files are not + created on disk unless a request succeeds. + +## 4.1.3 - 2014-07-15 + +* Various fixes to multipart/form-data POST uploads +* Wrapping function.php in an if-statement to ensure Guzzle can be used + globally and in a Composer install +* Fixed an issue with generating and merging in events to an event array +* POST headers are only applied before sending a request to allow you to change + the query aggregator used before uploading +* Added much more robust query string parsing +* Fixed various parsing and normalization issues with URLs +* Fixing an issue where multi-valued headers were not being utilized correctly + in the StreamAdapter + +## 4.1.2 - 2014-06-18 + +* Added support for sending payloads with GET requests + +## 4.1.1 - 2014-06-08 + +* Fixed an issue related to using custom message factory options in subclasses +* Fixed an issue with nested form fields in a multi-part POST +* Fixed an issue with using the `json` request option for POST requests +* Added `ToArrayInterface` to `GuzzleHttp\Cookie\CookieJar` + +## 4.1.0 - 2014-05-27 + +* Added a `json` request option to easily serialize JSON payloads. +* Added a `GuzzleHttp\json_decode()` wrapper to safely parse JSON. +* Added `setPort()` and `getPort()` to `GuzzleHttp\Message\RequestInterface`. +* Added the ability to provide an emitter to a client in the client constructor. +* Added the ability to persist a cookie session using $_SESSION. +* Added a trait that can be used to add event listeners to an iterator. +* Removed request method constants from RequestInterface. +* Fixed warning when invalid request start-lines are received. +* Updated MessageFactory to work with custom request option methods. +* Updated cacert bundle to latest build. + +4.0.2 (2014-04-16) +------------------ + +* Proxy requests using the StreamAdapter now properly use request_fulluri (#632) +* Added the ability to set scalars as POST fields (#628) + +## 4.0.1 - 2014-04-04 + +* The HTTP status code of a response is now set as the exception code of + RequestException objects. +* 303 redirects will now correctly switch from POST to GET requests. +* The default parallel adapter of a client now correctly uses the MultiAdapter. +* HasDataTrait now initializes the internal data array as an empty array so + that the toArray() method always returns an array. + +## 4.0.0 - 2014-03-29 + +* For information on changes and upgrading, see: + https://github.com/guzzle/guzzle/blob/master/UPGRADING.md#3x-to-40 +* Added `GuzzleHttp\batch()` as a convenience function for sending requests in + parallel without needing to write asynchronous code. +* Restructured how events are added to `GuzzleHttp\ClientInterface::sendAll()`. + You can now pass a callable or an array of associative arrays where each + associative array contains the "fn", "priority", and "once" keys. + +## 4.0.0.rc-2 - 2014-03-25 + +* Removed `getConfig()` and `setConfig()` from clients to avoid confusion + around whether things like base_url, message_factory, etc. should be able to + be retrieved or modified. +* Added `getDefaultOption()` and `setDefaultOption()` to ClientInterface +* functions.php functions were renamed using snake_case to match PHP idioms +* Added support for `HTTP_PROXY`, `HTTPS_PROXY`, and + `GUZZLE_CURL_SELECT_TIMEOUT` environment variables +* Added the ability to specify custom `sendAll()` event priorities +* Added the ability to specify custom stream context options to the stream + adapter. +* Added a functions.php function for `get_path()` and `set_path()` +* CurlAdapter and MultiAdapter now use a callable to generate curl resources +* MockAdapter now properly reads a body and emits a `headers` event +* Updated Url class to check if a scheme and host are set before adding ":" + and "//". This allows empty Url (e.g., "") to be serialized as "". +* Parsing invalid XML no longer emits warnings +* Curl classes now properly throw AdapterExceptions +* Various performance optimizations +* Streams are created with the faster `Stream\create()` function +* Marked deprecation_proxy() as internal +* Test server is now a collection of static methods on a class + +## 4.0.0-rc.1 - 2014-03-15 + +* See https://github.com/guzzle/guzzle/blob/master/UPGRADING.md#3x-to-40 + +## 3.8.1 - 2014-01-28 + +* Bug: Always using GET requests when redirecting from a 303 response +* Bug: CURLOPT_SSL_VERIFYHOST is now correctly set to false when setting `$certificateAuthority` to false in + `Guzzle\Http\ClientInterface::setSslVerification()` +* Bug: RedirectPlugin now uses strict RFC 3986 compliance when combining a base URL with a relative URL +* Bug: The body of a request can now be set to `"0"` +* Sending PHP stream requests no longer forces `HTTP/1.0` +* Adding more information to ExceptionCollection exceptions so that users have more context, including a stack trace of + each sub-exception +* Updated the `$ref` attribute in service descriptions to merge over any existing parameters of a schema (rather than + clobbering everything). +* Merging URLs will now use the query string object from the relative URL (thus allowing custom query aggregators) +* Query strings are now parsed in a way that they do no convert empty keys with no value to have a dangling `=`. + For example `foo&bar=baz` is now correctly parsed and recognized as `foo&bar=baz` rather than `foo=&bar=baz`. +* Now properly escaping the regular expression delimiter when matching Cookie domains. +* Network access is now disabled when loading XML documents + +## 3.8.0 - 2013-12-05 + +* Added the ability to define a POST name for a file +* JSON response parsing now properly walks additionalProperties +* cURL error code 18 is now retried automatically in the BackoffPlugin +* Fixed a cURL error when URLs contain fragments +* Fixed an issue in the BackoffPlugin retry event where it was trying to access all exceptions as if they were + CurlExceptions +* CURLOPT_PROGRESS function fix for PHP 5.5 (69fcc1e) +* Added the ability for Guzzle to work with older versions of cURL that do not support `CURLOPT_TIMEOUT_MS` +* Fixed a bug that was encountered when parsing empty header parameters +* UriTemplate now has a `setRegex()` method to match the docs +* The `debug` request parameter now checks if it is truthy rather than if it exists +* Setting the `debug` request parameter to true shows verbose cURL output instead of using the LogPlugin +* Added the ability to combine URLs using strict RFC 3986 compliance +* Command objects can now return the validation errors encountered by the command +* Various fixes to cache revalidation (#437 and 29797e5) +* Various fixes to the AsyncPlugin +* Cleaned up build scripts + +## 3.7.4 - 2013-10-02 + +* Bug fix: 0 is now an allowed value in a description parameter that has a default value (#430) +* Bug fix: SchemaFormatter now returns an integer when formatting to a Unix timestamp + (see https://github.com/aws/aws-sdk-php/issues/147) +* Bug fix: Cleaned up and fixed URL dot segment removal to properly resolve internal dots +* Minimum PHP version is now properly specified as 5.3.3 (up from 5.3.2) (#420) +* Updated the bundled cacert.pem (#419) +* OauthPlugin now supports adding authentication to headers or query string (#425) + +## 3.7.3 - 2013-09-08 + +* Added the ability to get the exception associated with a request/command when using `MultiTransferException` and + `CommandTransferException`. +* Setting `additionalParameters` of a response to false is now honored when parsing responses with a service description +* Schemas are only injected into response models when explicitly configured. +* No longer guessing Content-Type based on the path of a request. Content-Type is now only guessed based on the path of + an EntityBody. +* Bug fix: ChunkedIterator can now properly chunk a \Traversable as well as an \Iterator. +* Bug fix: FilterIterator now relies on `\Iterator` instead of `\Traversable`. +* Bug fix: Gracefully handling malformed responses in RequestMediator::writeResponseBody() +* Bug fix: Replaced call to canCache with canCacheRequest in the CallbackCanCacheStrategy of the CachePlugin +* Bug fix: Visiting XML attributes first before visiting XML children when serializing requests +* Bug fix: Properly parsing headers that contain commas contained in quotes +* Bug fix: mimetype guessing based on a filename is now case-insensitive + +## 3.7.2 - 2013-08-02 + +* Bug fix: Properly URL encoding paths when using the PHP-only version of the UriTemplate expander + See https://github.com/guzzle/guzzle/issues/371 +* Bug fix: Cookie domains are now matched correctly according to RFC 6265 + See https://github.com/guzzle/guzzle/issues/377 +* Bug fix: GET parameters are now used when calculating an OAuth signature +* Bug fix: Fixed an issue with cache revalidation where the If-None-Match header was being double quoted +* `Guzzle\Common\AbstractHasDispatcher::dispatch()` now returns the event that was dispatched +* `Guzzle\Http\QueryString::factory()` now guesses the most appropriate query aggregator to used based on the input. + See https://github.com/guzzle/guzzle/issues/379 +* Added a way to add custom domain objects to service description parsing using the `operation.parse_class` event. See + https://github.com/guzzle/guzzle/pull/380 +* cURL multi cleanup and optimizations + +## 3.7.1 - 2013-07-05 + +* Bug fix: Setting default options on a client now works +* Bug fix: Setting options on HEAD requests now works. See #352 +* Bug fix: Moving stream factory before send event to before building the stream. See #353 +* Bug fix: Cookies no longer match on IP addresses per RFC 6265 +* Bug fix: Correctly parsing header parameters that are in `<>` and quotes +* Added `cert` and `ssl_key` as request options +* `Host` header can now diverge from the host part of a URL if the header is set manually +* `Guzzle\Service\Command\LocationVisitor\Request\XmlVisitor` was rewritten to change from using SimpleXML to XMLWriter +* OAuth parameters are only added via the plugin if they aren't already set +* Exceptions are now thrown when a URL cannot be parsed +* Returning `false` if `Guzzle\Http\EntityBody::getContentMd5()` fails +* Not setting a `Content-MD5` on a command if calculating the Content-MD5 fails via the CommandContentMd5Plugin + +## 3.7.0 - 2013-06-10 + +* See UPGRADING.md for more information on how to upgrade. +* Requests now support the ability to specify an array of $options when creating a request to more easily modify a + request. You can pass a 'request.options' configuration setting to a client to apply default request options to + every request created by a client (e.g. default query string variables, headers, curl options, etc.). +* Added a static facade class that allows you to use Guzzle with static methods and mount the class to `\Guzzle`. + See `Guzzle\Http\StaticClient::mount`. +* Added `command.request_options` to `Guzzle\Service\Command\AbstractCommand` to pass request options to requests + created by a command (e.g. custom headers, query string variables, timeout settings, etc.). +* Stream size in `Guzzle\Stream\PhpStreamRequestFactory` will now be set if Content-Length is returned in the + headers of a response +* Added `Guzzle\Common\Collection::setPath($path, $value)` to set a value into an array using a nested key + (e.g. `$collection->setPath('foo/baz/bar', 'test'); echo $collection['foo']['bar']['bar'];`) +* ServiceBuilders now support storing and retrieving arbitrary data +* CachePlugin can now purge all resources for a given URI +* CachePlugin can automatically purge matching cached items when a non-idempotent request is sent to a resource +* CachePlugin now uses the Vary header to determine if a resource is a cache hit +* `Guzzle\Http\Message\Response` now implements `\Serializable` +* Added `Guzzle\Cache\CacheAdapterFactory::fromCache()` to more easily create cache adapters +* `Guzzle\Service\ClientInterface::execute()` now accepts an array, single command, or Traversable +* Fixed a bug in `Guzzle\Http\Message\Header\Link::addLink()` +* Better handling of calculating the size of a stream in `Guzzle\Stream\Stream` using fstat() and caching the size +* `Guzzle\Common\Exception\ExceptionCollection` now creates a more readable exception message +* Fixing BC break: Added back the MonologLogAdapter implementation rather than extending from PsrLog so that older + Symfony users can still use the old version of Monolog. +* Fixing BC break: Added the implementation back in for `Guzzle\Http\Message\AbstractMessage::getTokenizedHeader()`. + Now triggering an E_USER_DEPRECATED warning when used. Use `$message->getHeader()->parseParams()`. +* Several performance improvements to `Guzzle\Common\Collection` +* Added an `$options` argument to the end of the following methods of `Guzzle\Http\ClientInterface`: + createRequest, head, delete, put, patch, post, options, prepareRequest +* Added an `$options` argument to the end of `Guzzle\Http\Message\Request\RequestFactoryInterface::createRequest()` +* Added an `applyOptions()` method to `Guzzle\Http\Message\Request\RequestFactoryInterface` +* Changed `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $body = null)` to + `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $options = array())`. You can still pass in a + resource, string, or EntityBody into the $options parameter to specify the download location of the response. +* Changed `Guzzle\Common\Collection::__construct($data)` to no longer accepts a null value for `$data` but a + default `array()` +* Added `Guzzle\Stream\StreamInterface::isRepeatable` +* Removed `Guzzle\Http\ClientInterface::setDefaultHeaders(). Use + $client->getConfig()->setPath('request.options/headers/{header_name}', 'value')`. or + $client->getConfig()->setPath('request.options/headers', array('header_name' => 'value'))`. +* Removed `Guzzle\Http\ClientInterface::getDefaultHeaders(). Use $client->getConfig()->getPath('request.options/headers')`. +* Removed `Guzzle\Http\ClientInterface::expandTemplate()` +* Removed `Guzzle\Http\ClientInterface::setRequestFactory()` +* Removed `Guzzle\Http\ClientInterface::getCurlMulti()` +* Removed `Guzzle\Http\Message\RequestInterface::canCache` +* Removed `Guzzle\Http\Message\RequestInterface::setIsRedirect` +* Removed `Guzzle\Http\Message\RequestInterface::isRedirect` +* Made `Guzzle\Http\Client::expandTemplate` and `getUriTemplate` protected methods. +* You can now enable E_USER_DEPRECATED warnings to see if you are using a deprecated method by setting + `Guzzle\Common\Version::$emitWarnings` to true. +* Marked `Guzzle\Http\Message\Request::isResponseBodyRepeatable()` as deprecated. Use + `$request->getResponseBody()->isRepeatable()` instead. +* Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use + `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. +* Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use + `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. +* Marked `Guzzle\Http\Message\Request::setIsRedirect()` as deprecated. Use the HistoryPlugin instead. +* Marked `Guzzle\Http\Message\Request::isRedirect()` as deprecated. Use the HistoryPlugin instead. +* Marked `Guzzle\Cache\CacheAdapterFactory::factory()` as deprecated +* Marked 'command.headers', 'command.response_body' and 'command.on_complete' as deprecated for AbstractCommand. + These will work through Guzzle 4.0 +* Marked 'request.params' for `Guzzle\Http\Client` as deprecated. Use [request.options][params]. +* Marked `Guzzle\Service\Client::enableMagicMethods()` as deprecated. Magic methods can no longer be disabled on a Guzzle\Service\Client. +* Marked `Guzzle\Service\Client::getDefaultHeaders()` as deprecated. Use $client->getConfig()->getPath('request.options/headers')`. +* Marked `Guzzle\Service\Client::setDefaultHeaders()` as deprecated. Use $client->getConfig()->setPath('request.options/headers/{header_name}', 'value')`. +* Marked `Guzzle\Parser\Url\UrlParser` as deprecated. Just use PHP's `parse_url()` and percent encode your UTF-8. +* Marked `Guzzle\Common\Collection::inject()` as deprecated. +* Marked `Guzzle\Plugin\CurlAuth\CurlAuthPlugin` as deprecated. Use `$client->getConfig()->setPath('request.options/auth', array('user', 'pass', 'Basic|Digest');` +* CacheKeyProviderInterface and DefaultCacheKeyProvider are no longer used. All of this logic is handled in a + CacheStorageInterface. These two objects and interface will be removed in a future version. +* Always setting X-cache headers on cached responses +* Default cache TTLs are now handled by the CacheStorageInterface of a CachePlugin +* `CacheStorageInterface::cache($key, Response $response, $ttl = null)` has changed to `cache(RequestInterface + $request, Response $response);` +* `CacheStorageInterface::fetch($key)` has changed to `fetch(RequestInterface $request);` +* `CacheStorageInterface::delete($key)` has changed to `delete(RequestInterface $request);` +* Added `CacheStorageInterface::purge($url)` +* `DefaultRevalidation::__construct(CacheKeyProviderInterface $cacheKey, CacheStorageInterface $cache, CachePlugin + $plugin)` has changed to `DefaultRevalidation::__construct(CacheStorageInterface $cache, + CanCacheStrategyInterface $canCache = null)` +* Added `RevalidationInterface::shouldRevalidate(RequestInterface $request, Response $response)` + +## 3.6.0 - 2013-05-29 + +* ServiceDescription now implements ToArrayInterface +* Added command.hidden_params to blacklist certain headers from being treated as additionalParameters +* Guzzle can now correctly parse incomplete URLs +* Mixed casing of headers are now forced to be a single consistent casing across all values for that header. +* Messages internally use a HeaderCollection object to delegate handling case-insensitive header resolution +* Removed the whole changedHeader() function system of messages because all header changes now go through addHeader(). +* Specific header implementations can be created for complex headers. When a message creates a header, it uses a + HeaderFactory which can map specific headers to specific header classes. There is now a Link header and + CacheControl header implementation. +* Removed from interface: Guzzle\Http\ClientInterface::setUriTemplate +* Removed from interface: Guzzle\Http\ClientInterface::setCurlMulti() +* Removed Guzzle\Http\Message\Request::receivedRequestHeader() and implemented this functionality in + Guzzle\Http\Curl\RequestMediator +* Removed the optional $asString parameter from MessageInterface::getHeader(). Just cast the header to a string. +* Removed the optional $tryChunkedTransfer option from Guzzle\Http\Message\EntityEnclosingRequestInterface +* Removed the $asObjects argument from Guzzle\Http\Message\MessageInterface::getHeaders() +* Removed Guzzle\Parser\ParserRegister::get(). Use getParser() +* Removed Guzzle\Parser\ParserRegister::set(). Use registerParser(). +* All response header helper functions return a string rather than mixing Header objects and strings inconsistently +* Removed cURL blacklist support. This is no longer necessary now that Expect, Accept, etc. are managed by Guzzle + directly via interfaces +* Removed the injecting of a request object onto a response object. The methods to get and set a request still exist + but are a no-op until removed. +* Most classes that used to require a `Guzzle\Service\Command\CommandInterface` typehint now request a + `Guzzle\Service\Command\ArrayCommandInterface`. +* Added `Guzzle\Http\Message\RequestInterface::startResponse()` to the RequestInterface to handle injecting a response + on a request while the request is still being transferred +* The ability to case-insensitively search for header values +* Guzzle\Http\Message\Header::hasExactHeader +* Guzzle\Http\Message\Header::raw. Use getAll() +* Deprecated cache control specific methods on Guzzle\Http\Message\AbstractMessage. Use the CacheControl header object + instead. +* `Guzzle\Service\Command\CommandInterface` now extends from ToArrayInterface and ArrayAccess +* Added the ability to cast Model objects to a string to view debug information. + +## 3.5.0 - 2013-05-13 + +* Bug: Fixed a regression so that request responses are parsed only once per oncomplete event rather than multiple times +* Bug: Better cleanup of one-time events across the board (when an event is meant to fire once, it will now remove + itself from the EventDispatcher) +* Bug: `Guzzle\Log\MessageFormatter` now properly writes "total_time" and "connect_time" values +* Bug: Cloning an EntityEnclosingRequest now clones the EntityBody too +* Bug: Fixed an undefined index error when parsing nested JSON responses with a sentAs parameter that reference a + non-existent key +* Bug: All __call() method arguments are now required (helps with mocking frameworks) +* Deprecating Response::getRequest() and now using a shallow clone of a request object to remove a circular reference + to help with refcount based garbage collection of resources created by sending a request +* Deprecating ZF1 cache and log adapters. These will be removed in the next major version. +* Deprecating `Response::getPreviousResponse()` (method signature still exists, but it's deprecated). Use the + HistoryPlugin for a history. +* Added a `responseBody` alias for the `response_body` location +* Refactored internals to no longer rely on Response::getRequest() +* HistoryPlugin can now be cast to a string +* HistoryPlugin now logs transactions rather than requests and responses to more accurately keep track of the requests + and responses that are sent over the wire +* Added `getEffectiveUrl()` and `getRedirectCount()` to Response objects + +## 3.4.3 - 2013-04-30 + +* Bug fix: Fixing bug introduced in 3.4.2 where redirect responses are duplicated on the final redirected response +* Added a check to re-extract the temp cacert bundle from the phar before sending each request + +## 3.4.2 - 2013-04-29 + +* Bug fix: Stream objects now work correctly with "a" and "a+" modes +* Bug fix: Removing `Transfer-Encoding: chunked` header when a Content-Length is present +* Bug fix: AsyncPlugin no longer forces HEAD requests +* Bug fix: DateTime timezones are now properly handled when using the service description schema formatter +* Bug fix: CachePlugin now properly handles stale-if-error directives when a request to the origin server fails +* Setting a response on a request will write to the custom request body from the response body if one is specified +* LogPlugin now writes to php://output when STDERR is undefined +* Added the ability to set multiple POST files for the same key in a single call +* application/x-www-form-urlencoded POSTs now use the utf-8 charset by default +* Added the ability to queue CurlExceptions to the MockPlugin +* Cleaned up how manual responses are queued on requests (removed "queued_response" and now using request.before_send) +* Configuration loading now allows remote files + +## 3.4.1 - 2013-04-16 + +* Large refactoring to how CurlMulti handles work. There is now a proxy that sits in front of a pool of CurlMulti + handles. This greatly simplifies the implementation, fixes a couple bugs, and provides a small performance boost. +* Exceptions are now properly grouped when sending requests in parallel +* Redirects are now properly aggregated when a multi transaction fails +* Redirects now set the response on the original object even in the event of a failure +* Bug fix: Model names are now properly set even when using $refs +* Added support for PHP 5.5's CurlFile to prevent warnings with the deprecated @ syntax +* Added support for oauth_callback in OAuth signatures +* Added support for oauth_verifier in OAuth signatures +* Added support to attempt to retrieve a command first literally, then ucfirst, the with inflection + +## 3.4.0 - 2013-04-11 + +* Bug fix: URLs are now resolved correctly based on https://tools.ietf.org/html/rfc3986#section-5.2. #289 +* Bug fix: Absolute URLs with a path in a service description will now properly override the base URL. #289 +* Bug fix: Parsing a query string with a single PHP array value will now result in an array. #263 +* Bug fix: Better normalization of the User-Agent header to prevent duplicate headers. #264. +* Bug fix: Added `number` type to service descriptions. +* Bug fix: empty parameters are removed from an OAuth signature +* Bug fix: Revalidating a cache entry prefers the Last-Modified over the Date header +* Bug fix: Fixed "array to string" error when validating a union of types in a service description +* Bug fix: Removed code that attempted to determine the size of a stream when data is written to the stream +* Bug fix: Not including an `oauth_token` if the value is null in the OauthPlugin. +* Bug fix: Now correctly aggregating successful requests and failed requests in CurlMulti when a redirect occurs. +* The new default CURLOPT_TIMEOUT setting has been increased to 150 seconds so that Guzzle works on poor connections. +* Added a feature to EntityEnclosingRequest::setBody() that will automatically set the Content-Type of the request if + the Content-Type can be determined based on the entity body or the path of the request. +* Added the ability to overwrite configuration settings in a client when grabbing a throwaway client from a builder. +* Added support for a PSR-3 LogAdapter. +* Added a `command.after_prepare` event +* Added `oauth_callback` parameter to the OauthPlugin +* Added the ability to create a custom stream class when using a stream factory +* Added a CachingEntityBody decorator +* Added support for `additionalParameters` in service descriptions to define how custom parameters are serialized. +* The bundled SSL certificate is now provided in the phar file and extracted when running Guzzle from a phar. +* You can now send any EntityEnclosingRequest with POST fields or POST files and cURL will handle creating bodies +* POST requests using a custom entity body are now treated exactly like PUT requests but with a custom cURL method. This + means that the redirect behavior of POST requests with custom bodies will not be the same as POST requests that use + POST fields or files (the latter is only used when emulating a form POST in the browser). +* Lots of cleanup to CurlHandle::factory and RequestFactory::createRequest + +## 3.3.1 - 2013-03-10 + +* Added the ability to create PHP streaming responses from HTTP requests +* Bug fix: Running any filters when parsing response headers with service descriptions +* Bug fix: OauthPlugin fixes to allow for multi-dimensional array signing, and sorting parameters before signing +* Bug fix: Removed the adding of default empty arrays and false Booleans to responses in order to be consistent across + response location visitors. +* Bug fix: Removed the possibility of creating configuration files with circular dependencies +* RequestFactory::create() now uses the key of a POST file when setting the POST file name +* Added xmlAllowEmpty to serialize an XML body even if no XML specific parameters are set + +## 3.3.0 - 2013-03-03 + +* A large number of performance optimizations have been made +* Bug fix: Added 'wb' as a valid write mode for streams +* Bug fix: `Guzzle\Http\Message\Response::json()` now allows scalar values to be returned +* Bug fix: Fixed bug in `Guzzle\Http\Message\Response` where wrapping quotes were stripped from `getEtag()` +* BC: Removed `Guzzle\Http\Utils` class +* BC: Setting a service description on a client will no longer modify the client's command factories. +* BC: Emitting IO events from a RequestMediator is now a parameter that must be set in a request's curl options using + the 'emit_io' key. This was previously set under a request's parameters using 'curl.emit_io' +* BC: `Guzzle\Stream\Stream::getWrapper()` and `Guzzle\Stream\Stream::getSteamType()` are no longer converted to + lowercase +* Operation parameter objects are now lazy loaded internally +* Added ErrorResponsePlugin that can throw errors for responses defined in service description operations' errorResponses +* Added support for instantiating responseType=class responseClass classes. Classes must implement + `Guzzle\Service\Command\ResponseClassInterface` +* Added support for additionalProperties for top-level parameters in responseType=model responseClasses. These + additional properties also support locations and can be used to parse JSON responses where the outermost part of the + JSON is an array +* Added support for nested renaming of JSON models (rename sentAs to name) +* CachePlugin + * Added support for stale-if-error so that the CachePlugin can now serve stale content from the cache on error + * Debug headers can now added to cached response in the CachePlugin + +## 3.2.0 - 2013-02-14 + +* CurlMulti is no longer reused globally. A new multi object is created per-client. This helps to isolate clients. +* URLs with no path no longer contain a "/" by default +* Guzzle\Http\QueryString does no longer manages the leading "?". This is now handled in Guzzle\Http\Url. +* BadResponseException no longer includes the full request and response message +* Adding setData() to Guzzle\Service\Description\ServiceDescriptionInterface +* Adding getResponseBody() to Guzzle\Http\Message\RequestInterface +* Various updates to classes to use ServiceDescriptionInterface type hints rather than ServiceDescription +* Header values can now be normalized into distinct values when multiple headers are combined with a comma separated list +* xmlEncoding can now be customized for the XML declaration of a XML service description operation +* Guzzle\Http\QueryString now uses Guzzle\Http\QueryAggregator\QueryAggregatorInterface objects to add custom value + aggregation and no longer uses callbacks +* The URL encoding implementation of Guzzle\Http\QueryString can now be customized +* Bug fix: Filters were not always invoked for array service description parameters +* Bug fix: Redirects now use a target response body rather than a temporary response body +* Bug fix: The default exponential backoff BackoffPlugin was not giving when the request threshold was exceeded +* Bug fix: Guzzle now takes the first found value when grabbing Cache-Control directives + +## 3.1.2 - 2013-01-27 + +* Refactored how operation responses are parsed. Visitors now include a before() method responsible for parsing the + response body. For example, the XmlVisitor now parses the XML response into an array in the before() method. +* Fixed an issue where cURL would not automatically decompress responses when the Accept-Encoding header was sent +* CURLOPT_SSL_VERIFYHOST is never set to 1 because it is deprecated (see 5e0ff2ef20f839e19d1eeb298f90ba3598784444) +* Fixed a bug where redirect responses were not chained correctly using getPreviousResponse() +* Setting default headers on a client after setting the user-agent will not erase the user-agent setting + +## 3.1.1 - 2013-01-20 + +* Adding wildcard support to Guzzle\Common\Collection::getPath() +* Adding alias support to ServiceBuilder configs +* Adding Guzzle\Service\Resource\CompositeResourceIteratorFactory and cleaning up factory interface + +## 3.1.0 - 2013-01-12 + +* BC: CurlException now extends from RequestException rather than BadResponseException +* BC: Renamed Guzzle\Plugin\Cache\CanCacheStrategyInterface::canCache() to canCacheRequest() and added CanCacheResponse() +* Added getData to ServiceDescriptionInterface +* Added context array to RequestInterface::setState() +* Bug: Removing hard dependency on the BackoffPlugin from Guzzle\Http +* Bug: Adding required content-type when JSON request visitor adds JSON to a command +* Bug: Fixing the serialization of a service description with custom data +* Made it easier to deal with exceptions thrown when transferring commands or requests in parallel by providing + an array of successful and failed responses +* Moved getPath from Guzzle\Service\Resource\Model to Guzzle\Common\Collection +* Added Guzzle\Http\IoEmittingEntityBody +* Moved command filtration from validators to location visitors +* Added `extends` attributes to service description parameters +* Added getModels to ServiceDescriptionInterface + +## 3.0.7 - 2012-12-19 + +* Fixing phar detection when forcing a cacert to system if null or true +* Allowing filename to be passed to `Guzzle\Http\Message\Request::setResponseBody()` +* Cleaning up `Guzzle\Common\Collection::inject` method +* Adding a response_body location to service descriptions + +## 3.0.6 - 2012-12-09 + +* CurlMulti performance improvements +* Adding setErrorResponses() to Operation +* composer.json tweaks + +## 3.0.5 - 2012-11-18 + +* Bug: Fixing an infinite recursion bug caused from revalidating with the CachePlugin +* Bug: Response body can now be a string containing "0" +* Bug: Using Guzzle inside of a phar uses system by default but now allows for a custom cacert +* Bug: QueryString::fromString now properly parses query string parameters that contain equal signs +* Added support for XML attributes in service description responses +* DefaultRequestSerializer now supports array URI parameter values for URI template expansion +* Added better mimetype guessing to requests and post files + +## 3.0.4 - 2012-11-11 + +* Bug: Fixed a bug when adding multiple cookies to a request to use the correct glue value +* Bug: Cookies can now be added that have a name, domain, or value set to "0" +* Bug: Using the system cacert bundle when using the Phar +* Added json and xml methods to Response to make it easier to parse JSON and XML response data into data structures +* Enhanced cookie jar de-duplication +* Added the ability to enable strict cookie jars that throw exceptions when invalid cookies are added +* Added setStream to StreamInterface to actually make it possible to implement custom rewind behavior for entity bodies +* Added the ability to create any sort of hash for a stream rather than just an MD5 hash + +## 3.0.3 - 2012-11-04 + +* Implementing redirects in PHP rather than cURL +* Added PECL URI template extension and using as default parser if available +* Bug: Fixed Content-Length parsing of Response factory +* Adding rewind() method to entity bodies and streams. Allows for custom rewinding of non-repeatable streams. +* Adding ToArrayInterface throughout library +* Fixing OauthPlugin to create unique nonce values per request + +## 3.0.2 - 2012-10-25 + +* Magic methods are enabled by default on clients +* Magic methods return the result of a command +* Service clients no longer require a base_url option in the factory +* Bug: Fixed an issue with URI templates where null template variables were being expanded + +## 3.0.1 - 2012-10-22 + +* Models can now be used like regular collection objects by calling filter, map, etc. +* Models no longer require a Parameter structure or initial data in the constructor +* Added a custom AppendIterator to get around a PHP bug with the `\AppendIterator` + +## 3.0.0 - 2012-10-15 + +* Rewrote service description format to be based on Swagger + * Now based on JSON schema + * Added nested input structures and nested response models + * Support for JSON and XML input and output models + * Renamed `commands` to `operations` + * Removed dot class notation + * Removed custom types +* Broke the project into smaller top-level namespaces to be more component friendly +* Removed support for XML configs and descriptions. Use arrays or JSON files. +* Removed the Validation component and Inspector +* Moved all cookie code to Guzzle\Plugin\Cookie +* Magic methods on a Guzzle\Service\Client now return the command un-executed. +* Calling getResult() or getResponse() on a command will lazily execute the command if needed. +* Now shipping with cURL's CA certs and using it by default +* Added previousResponse() method to response objects +* No longer sending Accept and Accept-Encoding headers on every request +* Only sending an Expect header by default when a payload is greater than 1MB +* Added/moved client options: + * curl.blacklist to curl.option.blacklist + * Added ssl.certificate_authority +* Added a Guzzle\Iterator component +* Moved plugins from Guzzle\Http\Plugin to Guzzle\Plugin +* Added a more robust backoff retry strategy (replaced the ExponentialBackoffPlugin) +* Added a more robust caching plugin +* Added setBody to response objects +* Updating LogPlugin to use a more flexible MessageFormatter +* Added a completely revamped build process +* Cleaning up Collection class and removing default values from the get method +* Fixed ZF2 cache adapters + +## 2.8.8 - 2012-10-15 + +* Bug: Fixed a cookie issue that caused dot prefixed domains to not match where popular browsers did + +## 2.8.7 - 2012-09-30 + +* Bug: Fixed config file aliases for JSON includes +* Bug: Fixed cookie bug on a request object by using CookieParser to parse cookies on requests +* Bug: Removing the path to a file when sending a Content-Disposition header on a POST upload +* Bug: Hardening request and response parsing to account for missing parts +* Bug: Fixed PEAR packaging +* Bug: Fixed Request::getInfo +* Bug: Fixed cases where CURLM_CALL_MULTI_PERFORM return codes were causing curl transactions to fail +* Adding the ability for the namespace Iterator factory to look in multiple directories +* Added more getters/setters/removers from service descriptions +* Added the ability to remove POST fields from OAuth signatures +* OAuth plugin now supports 2-legged OAuth + +## 2.8.6 - 2012-09-05 + +* Added the ability to modify and build service descriptions +* Added the use of visitors to apply parameters to locations in service descriptions using the dynamic command +* Added a `json` parameter location +* Now allowing dot notation for classes in the CacheAdapterFactory +* Using the union of two arrays rather than an array_merge when extending service builder services and service params +* Ensuring that a service is a string before doing strpos() checks on it when substituting services for references + in service builder config files. +* Services defined in two different config files that include one another will by default replace the previously + defined service, but you can now create services that extend themselves and merge their settings over the previous +* The JsonLoader now supports aliasing filenames with different filenames. This allows you to alias something like + '_default' with a default JSON configuration file. + +## 2.8.5 - 2012-08-29 + +* Bug: Suppressed empty arrays from URI templates +* Bug: Added the missing $options argument from ServiceDescription::factory to enable caching +* Added support for HTTP responses that do not contain a reason phrase in the start-line +* AbstractCommand commands are now invokable +* Added a way to get the data used when signing an Oauth request before a request is sent + +## 2.8.4 - 2012-08-15 + +* Bug: Custom delay time calculations are no longer ignored in the ExponentialBackoffPlugin +* Added the ability to transfer entity bodies as a string rather than streamed. This gets around curl error 65. Set `body_as_string` in a request's curl options to enable. +* Added a StreamInterface, EntityBodyInterface, and added ftell() to Guzzle\Common\Stream +* Added an AbstractEntityBodyDecorator and a ReadLimitEntityBody decorator to transfer only a subset of a decorated stream +* Stream and EntityBody objects will now return the file position to the previous position after a read required operation (e.g. getContentMd5()) +* Added additional response status codes +* Removed SSL information from the default User-Agent header +* DELETE requests can now send an entity body +* Added an EventDispatcher to the ExponentialBackoffPlugin and added an ExponentialBackoffLogger to log backoff retries +* Added the ability of the MockPlugin to consume mocked request bodies +* LogPlugin now exposes request and response objects in the extras array + +## 2.8.3 - 2012-07-30 + +* Bug: Fixed a case where empty POST requests were sent as GET requests +* Bug: Fixed a bug in ExponentialBackoffPlugin that caused fatal errors when retrying an EntityEnclosingRequest that does not have a body +* Bug: Setting the response body of a request to null after completing a request, not when setting the state of a request to new +* Added multiple inheritance to service description commands +* Added an ApiCommandInterface and added `getParamNames()` and `hasParam()` +* Removed the default 2mb size cutoff from the Md5ValidatorPlugin so that it now defaults to validating everything +* Changed CurlMulti::perform to pass a smaller timeout to CurlMulti::executeHandles + +## 2.8.2 - 2012-07-24 + +* Bug: Query string values set to 0 are no longer dropped from the query string +* Bug: A Collection object is no longer created each time a call is made to `Guzzle\Service\Command\AbstractCommand::getRequestHeaders()` +* Bug: `+` is now treated as an encoded space when parsing query strings +* QueryString and Collection performance improvements +* Allowing dot notation for class paths in filters attribute of a service descriptions + +## 2.8.1 - 2012-07-16 + +* Loosening Event Dispatcher dependency +* POST redirects can now be customized using CURLOPT_POSTREDIR + +## 2.8.0 - 2012-07-15 + +* BC: Guzzle\Http\Query + * Query strings with empty variables will always show an equal sign unless the variable is set to QueryString::BLANK (e.g. ?acl= vs ?acl) + * Changed isEncodingValues() and isEncodingFields() to isUrlEncoding() + * Changed setEncodeValues(bool) and setEncodeFields(bool) to useUrlEncoding(bool) + * Changed the aggregation functions of QueryString to be static methods + * Can now use fromString() with querystrings that have a leading ? +* cURL configuration values can be specified in service descriptions using `curl.` prefixed parameters +* Content-Length is set to 0 before emitting the request.before_send event when sending an empty request body +* Cookies are no longer URL decoded by default +* Bug: URI template variables set to null are no longer expanded + +## 2.7.2 - 2012-07-02 + +* BC: Moving things to get ready for subtree splits. Moving Inflection into Common. Moving Guzzle\Http\Parser to Guzzle\Parser. +* BC: Removing Guzzle\Common\Batch\Batch::count() and replacing it with isEmpty() +* CachePlugin now allows for a custom request parameter function to check if a request can be cached +* Bug fix: CachePlugin now only caches GET and HEAD requests by default +* Bug fix: Using header glue when transferring headers over the wire +* Allowing deeply nested arrays for composite variables in URI templates +* Batch divisors can now return iterators or arrays + +## 2.7.1 - 2012-06-26 + +* Minor patch to update version number in UA string +* Updating build process + +## 2.7.0 - 2012-06-25 + +* BC: Inflection classes moved to Guzzle\Inflection. No longer static methods. Can now inject custom inflectors into classes. +* BC: Removed magic setX methods from commands +* BC: Magic methods mapped to service description commands are now inflected in the command factory rather than the client __call() method +* Verbose cURL options are no longer enabled by default. Set curl.debug to true on a client to enable. +* Bug: Now allowing colons in a response start-line (e.g. HTTP/1.1 503 Service Unavailable: Back-end server is at capacity) +* Guzzle\Service\Resource\ResourceIteratorApplyBatched now internally uses the Guzzle\Common\Batch namespace +* Added Guzzle\Service\Plugin namespace and a PluginCollectionPlugin +* Added the ability to set POST fields and files in a service description +* Guzzle\Http\EntityBody::factory() now accepts objects with a __toString() method +* Adding a command.before_prepare event to clients +* Added BatchClosureTransfer and BatchClosureDivisor +* BatchTransferException now includes references to the batch divisor and transfer strategies +* Fixed some tests so that they pass more reliably +* Added Guzzle\Common\Log\ArrayLogAdapter + +## 2.6.6 - 2012-06-10 + +* BC: Removing Guzzle\Http\Plugin\BatchQueuePlugin +* BC: Removing Guzzle\Service\Command\CommandSet +* Adding generic batching system (replaces the batch queue plugin and command set) +* Updating ZF cache and log adapters and now using ZF's composer repository +* Bug: Setting the name of each ApiParam when creating through an ApiCommand +* Adding result_type, result_doc, deprecated, and doc_url to service descriptions +* Bug: Changed the default cookie header casing back to 'Cookie' + +## 2.6.5 - 2012-06-03 + +* BC: Renaming Guzzle\Http\Message\RequestInterface::getResourceUri() to getResource() +* BC: Removing unused AUTH_BASIC and AUTH_DIGEST constants from +* BC: Guzzle\Http\Cookie is now used to manage Set-Cookie data, not Cookie data +* BC: Renaming methods in the CookieJarInterface +* Moving almost all cookie logic out of the CookiePlugin and into the Cookie or CookieJar implementations +* Making the default glue for HTTP headers ';' instead of ',' +* Adding a removeValue to Guzzle\Http\Message\Header +* Adding getCookies() to request interface. +* Making it easier to add event subscribers to HasDispatcherInterface classes. Can now directly call addSubscriber() + +## 2.6.4 - 2012-05-30 + +* BC: Cleaning up how POST files are stored in EntityEnclosingRequest objects. Adding PostFile class. +* BC: Moving ApiCommand specific functionality from the Inspector and on to the ApiCommand +* Bug: Fixing magic method command calls on clients +* Bug: Email constraint only validates strings +* Bug: Aggregate POST fields when POST files are present in curl handle +* Bug: Fixing default User-Agent header +* Bug: Only appending or prepending parameters in commands if they are specified +* Bug: Not requiring response reason phrases or status codes to match a predefined list of codes +* Allowing the use of dot notation for class namespaces when using instance_of constraint +* Added any_match validation constraint +* Added an AsyncPlugin +* Passing request object to the calculateWait method of the ExponentialBackoffPlugin +* Allowing the result of a command object to be changed +* Parsing location and type sub values when instantiating a service description rather than over and over at runtime + +## 2.6.3 - 2012-05-23 + +* [BC] Guzzle\Common\FromConfigInterface no longer requires any config options. +* [BC] Refactoring how POST files are stored on an EntityEnclosingRequest. They are now separate from POST fields. +* You can now use an array of data when creating PUT request bodies in the request factory. +* Removing the requirement that HTTPS requests needed a Cache-Control: public directive to be cacheable. +* [Http] Adding support for Content-Type in multipart POST uploads per upload +* [Http] Added support for uploading multiple files using the same name (foo[0], foo[1]) +* Adding more POST data operations for easier manipulation of POST data. +* You can now set empty POST fields. +* The body of a request is only shown on EntityEnclosingRequest objects that do not use POST files. +* Split the Guzzle\Service\Inspector::validateConfig method into two methods. One to initialize when a command is created, and one to validate. +* CS updates + +## 2.6.2 - 2012-05-19 + +* [Http] Better handling of nested scope requests in CurlMulti. Requests are now always prepares in the send() method rather than the addRequest() method. + +## 2.6.1 - 2012-05-19 + +* [BC] Removing 'path' support in service descriptions. Use 'uri'. +* [BC] Guzzle\Service\Inspector::parseDocBlock is now protected. Adding getApiParamsForClass() with cache. +* [BC] Removing Guzzle\Common\NullObject. Use https://github.com/mtdowling/NullObject if you need it. +* [BC] Removing Guzzle\Common\XmlElement. +* All commands, both dynamic and concrete, have ApiCommand objects. +* Adding a fix for CurlMulti so that if all of the connections encounter some sort of curl error, then the loop exits. +* Adding checks to EntityEnclosingRequest so that empty POST files and fields are ignored. +* Making the method signature of Guzzle\Service\Builder\ServiceBuilder::factory more flexible. + +## 2.6.0 - 2012-05-15 + +* [BC] Moving Guzzle\Service\Builder to Guzzle\Service\Builder\ServiceBuilder +* [BC] Executing a Command returns the result of the command rather than the command +* [BC] Moving all HTTP parsing logic to Guzzle\Http\Parsers. Allows for faster C implementations if needed. +* [BC] Changing the Guzzle\Http\Message\Response::setProtocol() method to accept a protocol and version in separate args. +* [BC] Moving ResourceIterator* to Guzzle\Service\Resource +* [BC] Completely refactored ResourceIterators to iterate over a cloned command object +* [BC] Moved Guzzle\Http\UriTemplate to Guzzle\Http\Parser\UriTemplate\UriTemplate +* [BC] Guzzle\Guzzle is now deprecated +* Moving Guzzle\Common\Guzzle::inject to Guzzle\Common\Collection::inject +* Adding Guzzle\Version class to give version information about Guzzle +* Adding Guzzle\Http\Utils class to provide getDefaultUserAgent() and getHttpDate() +* Adding Guzzle\Curl\CurlVersion to manage caching curl_version() data +* ServiceDescription and ServiceBuilder are now cacheable using similar configs +* Changing the format of XML and JSON service builder configs. Backwards compatible. +* Cleaned up Cookie parsing +* Trimming the default Guzzle User-Agent header +* Adding a setOnComplete() method to Commands that is called when a command completes +* Keeping track of requests that were mocked in the MockPlugin +* Fixed a caching bug in the CacheAdapterFactory +* Inspector objects can be injected into a Command object +* Refactoring a lot of code and tests to be case insensitive when dealing with headers +* Adding Guzzle\Http\Message\HeaderComparison for easy comparison of HTTP headers using a DSL +* Adding the ability to set global option overrides to service builder configs +* Adding the ability to include other service builder config files from within XML and JSON files +* Moving the parseQuery method out of Url and on to QueryString::fromString() as a static factory method. + +## 2.5.0 - 2012-05-08 + +* Major performance improvements +* [BC] Simplifying Guzzle\Common\Collection. Please check to see if you are using features that are now deprecated. +* [BC] Using a custom validation system that allows a flyweight implementation for much faster validation. No longer using Symfony2 Validation component. +* [BC] No longer supporting "{{ }}" for injecting into command or UriTemplates. Use "{}" +* Added the ability to passed parameters to all requests created by a client +* Added callback functionality to the ExponentialBackoffPlugin +* Using microtime in ExponentialBackoffPlugin to allow more granular backoff strategies. +* Rewinding request stream bodies when retrying requests +* Exception is thrown when JSON response body cannot be decoded +* Added configurable magic method calls to clients and commands. This is off by default. +* Fixed a defect that added a hash to every parsed URL part +* Fixed duplicate none generation for OauthPlugin. +* Emitting an event each time a client is generated by a ServiceBuilder +* Using an ApiParams object instead of a Collection for parameters of an ApiCommand +* cache.* request parameters should be renamed to params.cache.* +* Added the ability to set arbitrary curl options on requests (disable_wire, progress, etc.). See CurlHandle. +* Added the ability to disable type validation of service descriptions +* ServiceDescriptions and ServiceBuilders are now Serializable diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/LICENSE b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/LICENSE new file mode 100644 index 0000000..d51aa69 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011 Michael Dowling, https://github.com/mtdowling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/README.md b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/README.md new file mode 100644 index 0000000..363d6f8 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/README.md @@ -0,0 +1,79 @@ +![Guzzle](.github/logo.png?raw=true) + +# Guzzle, PHP HTTP client + +[![Latest Version](https://img.shields.io/github/release/guzzle/guzzle.svg?style=flat-square)](https://github.com/guzzle/guzzle/releases) +[![Build Status](https://img.shields.io/github/workflow/status/guzzle/guzzle/CI?label=ci%20build&style=flat-square)](https://github.com/guzzle/guzzle/actions?query=workflow%3ACI) +[![Total Downloads](https://img.shields.io/packagist/dt/guzzlehttp/guzzle.svg?style=flat-square)](https://packagist.org/packages/guzzlehttp/guzzle) + +Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and +trivial to integrate with web services. + +- Simple interface for building query strings, POST requests, streaming large + uploads, streaming large downloads, using HTTP cookies, uploading JSON data, + etc... +- Can send both synchronous and asynchronous requests using the same interface. +- Uses PSR-7 interfaces for requests, responses, and streams. This allows you + to utilize other PSR-7 compatible libraries with Guzzle. +- Supports PSR-18 allowing interoperability between other PSR-18 HTTP Clients. +- Abstracts away the underlying HTTP transport, allowing you to write + environment and transport agnostic code; i.e., no hard dependency on cURL, + PHP streams, sockets, or non-blocking event loops. +- Middleware system allows you to augment and compose client behavior. + +```php +$client = new \GuzzleHttp\Client(); +$response = $client->request('GET', 'https://api.github.com/repos/guzzle/guzzle'); + +echo $response->getStatusCode(); // 200 +echo $response->getHeaderLine('content-type'); // 'application/json; charset=utf8' +echo $response->getBody(); // '{"id": 1420053, "name": "guzzle", ...}' + +// Send an asynchronous request. +$request = new \GuzzleHttp\Psr7\Request('GET', 'http://httpbin.org'); +$promise = $client->sendAsync($request)->then(function ($response) { + echo 'I completed! ' . $response->getBody(); +}); + +$promise->wait(); +``` + +## Help and docs + +We use GitHub issues only to discuss bugs and new features. For support please refer to: + +- [Documentation](http://guzzlephp.org/) +- [Stack Overflow](http://stackoverflow.com/questions/tagged/guzzle) +- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](http://slack.httplug.io/) +- [Gitter](https://gitter.im/guzzle/guzzle) + + +## Installing Guzzle + +The recommended way to install Guzzle is through +[Composer](https://getcomposer.org/). + +```bash +composer require guzzlehttp/guzzle +``` + + +## Version Guidance + +| Version | Status | Packagist | Namespace | Repo | Docs | PSR-7 | PHP Version | +|---------|------------|---------------------|--------------|---------------------|---------------------|-------|-------------| +| 3.x | EOL | `guzzle/guzzle` | `Guzzle` | [v3][guzzle-3-repo] | [v3][guzzle-3-docs] | No | >= 5.3.3 | +| 4.x | EOL | `guzzlehttp/guzzle` | `GuzzleHttp` | [v4][guzzle-4-repo] | N/A | No | >= 5.4 | +| 5.x | EOL | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No | >= 5.4 | +| 6.x | Security fixes | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes | >= 5.5 | +| 7.x | Latest | `guzzlehttp/guzzle` | `GuzzleHttp` | [v7][guzzle-7-repo] | [v7][guzzle-7-docs] | Yes | >= 7.2 | + +[guzzle-3-repo]: https://github.com/guzzle/guzzle3 +[guzzle-4-repo]: https://github.com/guzzle/guzzle/tree/4.x +[guzzle-5-repo]: https://github.com/guzzle/guzzle/tree/5.3 +[guzzle-6-repo]: https://github.com/guzzle/guzzle/tree/6.5 +[guzzle-7-repo]: https://github.com/guzzle/guzzle +[guzzle-3-docs]: http://guzzle3.readthedocs.org +[guzzle-5-docs]: http://docs.guzzlephp.org/en/5.3/ +[guzzle-6-docs]: http://docs.guzzlephp.org/en/6.5/ +[guzzle-7-docs]: http://docs.guzzlephp.org/en/latest/ diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/UPGRADING.md b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/UPGRADING.md new file mode 100644 index 0000000..45417a7 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/UPGRADING.md @@ -0,0 +1,1253 @@ +Guzzle Upgrade Guide +==================== + +6.0 to 7.0 +---------- + +In order to take advantage of the new features of PHP, Guzzle dropped the support +of PHP 5. The minimum supported PHP version is now PHP 7.2. Type hints and return +types for functions and methods have been added wherever possible. + +Please make sure: +- You are calling a function or a method with the correct type. +- If you extend a class of Guzzle; update all signatures on methods you override. + +#### Other backwards compatibility breaking changes + +- Class `GuzzleHttp\UriTemplate` is removed. +- Class `GuzzleHttp\Exception\SeekException` is removed. +- Classes `GuzzleHttp\Exception\BadResponseException`, `GuzzleHttp\Exception\ClientException`, + `GuzzleHttp\Exception\ServerException` can no longer be initialized with an empty + Response as argument. +- Class `GuzzleHttp\Exception\ConnectException` now extends `GuzzleHttp\Exception\TransferException` + instead of `GuzzleHttp\Exception\RequestException`. +- Function `GuzzleHttp\Exception\ConnectException::getResponse()` is removed. +- Function `GuzzleHttp\Exception\ConnectException::hasResponse()` is removed. +- Constant `GuzzleHttp\ClientInterface::VERSION` is removed. Added `GuzzleHttp\ClientInterface::MAJOR_VERSION` instead. +- Function `GuzzleHttp\Exception\RequestException::getResponseBodySummary` is removed. + Use `\GuzzleHttp\Psr7\get_message_body_summary` as an alternative. +- Function `GuzzleHttp\Cookie\CookieJar::getCookieValue` is removed. +- Request option `exception` is removed. Please use `http_errors`. +- Request option `save_to` is removed. Please use `sink`. +- Pool option `pool_size` is removed. Please use `concurrency`. +- We now look for environment variables in the `$_SERVER` super global, due to thread safety issues with `getenv`. We continue to fallback to `getenv` in CLI environments, for maximum compatibility. +- The `get`, `head`, `put`, `post`, `patch`, `delete`, `getAsync`, `headAsync`, `putAsync`, `postAsync`, `patchAsync`, and `deleteAsync` methods are now implemented as genuine methods on `GuzzleHttp\Client`, with strong typing. The original `__call` implementation remains unchanged for now, for maximum backwards compatibility, but won't be invoked under normal operation. +- The `log` middleware will log the errors with level `error` instead of `notice` +- Support for international domain names (IDN) is now disabled by default, and enabling it requires installing ext-intl, linked against a modern version of the C library (ICU 4.6 or higher). + +#### Native functions calls + +All internal native functions calls of Guzzle are now prefixed with a slash. This +change makes it impossible for method overloading by other libraries or applications. +Example: + +```php +// Before: +curl_version(); + +// After: +\curl_version(); +``` + +For the full diff you can check [here](https://github.com/guzzle/guzzle/compare/6.5.4..master). + +5.0 to 6.0 +---------- + +Guzzle now uses [PSR-7](https://www.php-fig.org/psr/psr-7/) for HTTP messages. +Due to the fact that these messages are immutable, this prompted a refactoring +of Guzzle to use a middleware based system rather than an event system. Any +HTTP message interaction (e.g., `GuzzleHttp\Message\Request`) need to be +updated to work with the new immutable PSR-7 request and response objects. Any +event listeners or subscribers need to be updated to become middleware +functions that wrap handlers (or are injected into a +`GuzzleHttp\HandlerStack`). + +- Removed `GuzzleHttp\BatchResults` +- Removed `GuzzleHttp\Collection` +- Removed `GuzzleHttp\HasDataTrait` +- Removed `GuzzleHttp\ToArrayInterface` +- The `guzzlehttp/streams` dependency has been removed. Stream functionality + is now present in the `GuzzleHttp\Psr7` namespace provided by the + `guzzlehttp/psr7` package. +- Guzzle no longer uses ReactPHP promises and now uses the + `guzzlehttp/promises` library. We use a custom promise library for three + significant reasons: + 1. React promises (at the time of writing this) are recursive. Promise + chaining and promise resolution will eventually blow the stack. Guzzle + promises are not recursive as they use a sort of trampolining technique. + Note: there has been movement in the React project to modify promises to + no longer utilize recursion. + 2. Guzzle needs to have the ability to synchronously block on a promise to + wait for a result. Guzzle promises allows this functionality (and does + not require the use of recursion). + 3. Because we need to be able to wait on a result, doing so using React + promises requires wrapping react promises with RingPHP futures. This + overhead is no longer needed, reducing stack sizes, reducing complexity, + and improving performance. +- `GuzzleHttp\Mimetypes` has been moved to a function in + `GuzzleHttp\Psr7\mimetype_from_extension` and + `GuzzleHttp\Psr7\mimetype_from_filename`. +- `GuzzleHttp\Query` and `GuzzleHttp\QueryParser` have been removed. Query + strings must now be passed into request objects as strings, or provided to + the `query` request option when creating requests with clients. The `query` + option uses PHP's `http_build_query` to convert an array to a string. If you + need a different serialization technique, you will need to pass the query + string in as a string. There are a couple helper functions that will make + working with query strings easier: `GuzzleHttp\Psr7\parse_query` and + `GuzzleHttp\Psr7\build_query`. +- Guzzle no longer has a dependency on RingPHP. Due to the use of a middleware + system based on PSR-7, using RingPHP and it's middleware system as well adds + more complexity than the benefits it provides. All HTTP handlers that were + present in RingPHP have been modified to work directly with PSR-7 messages + and placed in the `GuzzleHttp\Handler` namespace. This significantly reduces + complexity in Guzzle, removes a dependency, and improves performance. RingPHP + will be maintained for Guzzle 5 support, but will no longer be a part of + Guzzle 6. +- As Guzzle now uses a middleware based systems the event system and RingPHP + integration has been removed. Note: while the event system has been removed, + it is possible to add your own type of event system that is powered by the + middleware system. + - Removed the `Event` namespace. + - Removed the `Subscriber` namespace. + - Removed `Transaction` class + - Removed `RequestFsm` + - Removed `RingBridge` + - `GuzzleHttp\Subscriber\Cookie` is now provided by + `GuzzleHttp\Middleware::cookies` + - `GuzzleHttp\Subscriber\HttpError` is now provided by + `GuzzleHttp\Middleware::httpError` + - `GuzzleHttp\Subscriber\History` is now provided by + `GuzzleHttp\Middleware::history` + - `GuzzleHttp\Subscriber\Mock` is now provided by + `GuzzleHttp\Handler\MockHandler` + - `GuzzleHttp\Subscriber\Prepare` is now provided by + `GuzzleHttp\PrepareBodyMiddleware` + - `GuzzleHttp\Subscriber\Redirect` is now provided by + `GuzzleHttp\RedirectMiddleware` +- Guzzle now uses `Psr\Http\Message\UriInterface` (implements in + `GuzzleHttp\Psr7\Uri`) for URI support. `GuzzleHttp\Url` is now gone. +- Static functions in `GuzzleHttp\Utils` have been moved to namespaced + functions under the `GuzzleHttp` namespace. This requires either a Composer + based autoloader or you to include functions.php. +- `GuzzleHttp\ClientInterface::getDefaultOption` has been renamed to + `GuzzleHttp\ClientInterface::getConfig`. +- `GuzzleHttp\ClientInterface::setDefaultOption` has been removed. +- The `json` and `xml` methods of response objects has been removed. With the + migration to strictly adhering to PSR-7 as the interface for Guzzle messages, + adding methods to message interfaces would actually require Guzzle messages + to extend from PSR-7 messages rather then work with them directly. + +## Migrating to middleware + +The change to PSR-7 unfortunately required significant refactoring to Guzzle +due to the fact that PSR-7 messages are immutable. Guzzle 5 relied on an event +system from plugins. The event system relied on mutability of HTTP messages and +side effects in order to work. With immutable messages, you have to change your +workflow to become more about either returning a value (e.g., functional +middlewares) or setting a value on an object. Guzzle v6 has chosen the +functional middleware approach. + +Instead of using the event system to listen for things like the `before` event, +you now create a stack based middleware function that intercepts a request on +the way in and the promise of the response on the way out. This is a much +simpler and more predictable approach than the event system and works nicely +with PSR-7 middleware. Due to the use of promises, the middleware system is +also asynchronous. + +v5: + +```php +use GuzzleHttp\Event\BeforeEvent; +$client = new GuzzleHttp\Client(); +// Get the emitter and listen to the before event. +$client->getEmitter()->on('before', function (BeforeEvent $e) { + // Guzzle v5 events relied on mutation + $e->getRequest()->setHeader('X-Foo', 'Bar'); +}); +``` + +v6: + +In v6, you can modify the request before it is sent using the `mapRequest` +middleware. The idiomatic way in v6 to modify the request/response lifecycle is +to setup a handler middleware stack up front and inject the handler into a +client. + +```php +use GuzzleHttp\Middleware; +// Create a handler stack that has all of the default middlewares attached +$handler = GuzzleHttp\HandlerStack::create(); +// Push the handler onto the handler stack +$handler->push(Middleware::mapRequest(function (RequestInterface $request) { + // Notice that we have to return a request object + return $request->withHeader('X-Foo', 'Bar'); +})); +// Inject the handler into the client +$client = new GuzzleHttp\Client(['handler' => $handler]); +``` + +## POST Requests + +This version added the [`form_params`](http://guzzle.readthedocs.org/en/latest/request-options.html#form_params) +and `multipart` request options. `form_params` is an associative array of +strings or array of strings and is used to serialize an +`application/x-www-form-urlencoded` POST request. The +[`multipart`](http://guzzle.readthedocs.org/en/latest/request-options.html#multipart) +option is now used to send a multipart/form-data POST request. + +`GuzzleHttp\Post\PostFile` has been removed. Use the `multipart` option to add +POST files to a multipart/form-data request. + +The `body` option no longer accepts an array to send POST requests. Please use +`multipart` or `form_params` instead. + +The `base_url` option has been renamed to `base_uri`. + +4.x to 5.0 +---------- + +## Rewritten Adapter Layer + +Guzzle now uses [RingPHP](http://ringphp.readthedocs.org/en/latest) to send +HTTP requests. The `adapter` option in a `GuzzleHttp\Client` constructor +is still supported, but it has now been renamed to `handler`. Instead of +passing a `GuzzleHttp\Adapter\AdapterInterface`, you must now pass a PHP +`callable` that follows the RingPHP specification. + +## Removed Fluent Interfaces + +[Fluent interfaces were removed](https://ocramius.github.io/blog/fluent-interfaces-are-evil/) +from the following classes: + +- `GuzzleHttp\Collection` +- `GuzzleHttp\Url` +- `GuzzleHttp\Query` +- `GuzzleHttp\Post\PostBody` +- `GuzzleHttp\Cookie\SetCookie` + +## Removed functions.php + +Removed "functions.php", so that Guzzle is truly PSR-4 compliant. The following +functions can be used as replacements. + +- `GuzzleHttp\json_decode` -> `GuzzleHttp\Utils::jsonDecode` +- `GuzzleHttp\get_path` -> `GuzzleHttp\Utils::getPath` +- `GuzzleHttp\Utils::setPath` -> `GuzzleHttp\set_path` +- `GuzzleHttp\Pool::batch` -> `GuzzleHttp\batch`. This function is, however, + deprecated in favor of using `GuzzleHttp\Pool::batch()`. + +The "procedural" global client has been removed with no replacement (e.g., +`GuzzleHttp\get()`, `GuzzleHttp\post()`, etc.). Use a `GuzzleHttp\Client` +object as a replacement. + +## `throwImmediately` has been removed + +The concept of "throwImmediately" has been removed from exceptions and error +events. This control mechanism was used to stop a transfer of concurrent +requests from completing. This can now be handled by throwing the exception or +by cancelling a pool of requests or each outstanding future request +individually. + +## headers event has been removed + +Removed the "headers" event. This event was only useful for changing the +body a response once the headers of the response were known. You can implement +a similar behavior in a number of ways. One example might be to use a +FnStream that has access to the transaction being sent. For example, when the +first byte is written, you could check if the response headers match your +expectations, and if so, change the actual stream body that is being +written to. + +## Updates to HTTP Messages + +Removed the `asArray` parameter from +`GuzzleHttp\Message\MessageInterface::getHeader`. If you want to get a header +value as an array, then use the newly added `getHeaderAsArray()` method of +`MessageInterface`. This change makes the Guzzle interfaces compatible with +the PSR-7 interfaces. + +3.x to 4.0 +---------- + +## Overarching changes: + +- Now requires PHP 5.4 or greater. +- No longer requires cURL to send requests. +- Guzzle no longer wraps every exception it throws. Only exceptions that are + recoverable are now wrapped by Guzzle. +- Various namespaces have been removed or renamed. +- No longer requiring the Symfony EventDispatcher. A custom event dispatcher + based on the Symfony EventDispatcher is + now utilized in `GuzzleHttp\Event\EmitterInterface` (resulting in significant + speed and functionality improvements). + +Changes per Guzzle 3.x namespace are described below. + +## Batch + +The `Guzzle\Batch` namespace has been removed. This is best left to +third-parties to implement on top of Guzzle's core HTTP library. + +## Cache + +The `Guzzle\Cache` namespace has been removed. (Todo: No suitable replacement +has been implemented yet, but hoping to utilize a PSR cache interface). + +## Common + +- Removed all of the wrapped exceptions. It's better to use the standard PHP + library for unrecoverable exceptions. +- `FromConfigInterface` has been removed. +- `Guzzle\Common\Version` has been removed. The VERSION constant can be found + at `GuzzleHttp\ClientInterface::VERSION`. + +### Collection + +- `getAll` has been removed. Use `toArray` to convert a collection to an array. +- `inject` has been removed. +- `keySearch` has been removed. +- `getPath` no longer supports wildcard expressions. Use something better like + JMESPath for this. +- `setPath` now supports appending to an existing array via the `[]` notation. + +### Events + +Guzzle no longer requires Symfony's EventDispatcher component. Guzzle now uses +`GuzzleHttp\Event\Emitter`. + +- `Symfony\Component\EventDispatcher\EventDispatcherInterface` is replaced by + `GuzzleHttp\Event\EmitterInterface`. +- `Symfony\Component\EventDispatcher\EventDispatcher` is replaced by + `GuzzleHttp\Event\Emitter`. +- `Symfony\Component\EventDispatcher\Event` is replaced by + `GuzzleHttp\Event\Event`, and Guzzle now has an EventInterface in + `GuzzleHttp\Event\EventInterface`. +- `AbstractHasDispatcher` has moved to a trait, `HasEmitterTrait`, and + `HasDispatcherInterface` has moved to `HasEmitterInterface`. Retrieving the + event emitter of a request, client, etc. now uses the `getEmitter` method + rather than the `getDispatcher` method. + +#### Emitter + +- Use the `once()` method to add a listener that automatically removes itself + the first time it is invoked. +- Use the `listeners()` method to retrieve a list of event listeners rather than + the `getListeners()` method. +- Use `emit()` instead of `dispatch()` to emit an event from an emitter. +- Use `attach()` instead of `addSubscriber()` and `detach()` instead of + `removeSubscriber()`. + +```php +$mock = new Mock(); +// 3.x +$request->getEventDispatcher()->addSubscriber($mock); +$request->getEventDispatcher()->removeSubscriber($mock); +// 4.x +$request->getEmitter()->attach($mock); +$request->getEmitter()->detach($mock); +``` + +Use the `on()` method to add a listener rather than the `addListener()` method. + +```php +// 3.x +$request->getEventDispatcher()->addListener('foo', function (Event $event) { /* ... */ } ); +// 4.x +$request->getEmitter()->on('foo', function (Event $event, $name) { /* ... */ } ); +``` + +## Http + +### General changes + +- The cacert.pem certificate has been moved to `src/cacert.pem`. +- Added the concept of adapters that are used to transfer requests over the + wire. +- Simplified the event system. +- Sending requests in parallel is still possible, but batching is no longer a + concept of the HTTP layer. Instead, you must use the `complete` and `error` + events to asynchronously manage parallel request transfers. +- `Guzzle\Http\Url` has moved to `GuzzleHttp\Url`. +- `Guzzle\Http\QueryString` has moved to `GuzzleHttp\Query`. +- QueryAggregators have been rewritten so that they are simply callable + functions. +- `GuzzleHttp\StaticClient` has been removed. Use the functions provided in + `functions.php` for an easy to use static client instance. +- Exceptions in `GuzzleHttp\Exception` have been updated to all extend from + `GuzzleHttp\Exception\TransferException`. + +### Client + +Calling methods like `get()`, `post()`, `head()`, etc. no longer create and +return a request, but rather creates a request, sends the request, and returns +the response. + +```php +// 3.0 +$request = $client->get('/'); +$response = $request->send(); + +// 4.0 +$response = $client->get('/'); + +// or, to mirror the previous behavior +$request = $client->createRequest('GET', '/'); +$response = $client->send($request); +``` + +`GuzzleHttp\ClientInterface` has changed. + +- The `send` method no longer accepts more than one request. Use `sendAll` to + send multiple requests in parallel. +- `setUserAgent()` has been removed. Use a default request option instead. You + could, for example, do something like: + `$client->setConfig('defaults/headers/User-Agent', 'Foo/Bar ' . $client::getDefaultUserAgent())`. +- `setSslVerification()` has been removed. Use default request options instead, + like `$client->setConfig('defaults/verify', true)`. + +`GuzzleHttp\Client` has changed. + +- The constructor now accepts only an associative array. You can include a + `base_url` string or array to use a URI template as the base URL of a client. + You can also specify a `defaults` key that is an associative array of default + request options. You can pass an `adapter` to use a custom adapter, + `batch_adapter` to use a custom adapter for sending requests in parallel, or + a `message_factory` to change the factory used to create HTTP requests and + responses. +- The client no longer emits a `client.create_request` event. +- Creating requests with a client no longer automatically utilize a URI + template. You must pass an array into a creational method (e.g., + `createRequest`, `get`, `put`, etc.) in order to expand a URI template. + +### Messages + +Messages no longer have references to their counterparts (i.e., a request no +longer has a reference to it's response, and a response no loger has a +reference to its request). This association is now managed through a +`GuzzleHttp\Adapter\TransactionInterface` object. You can get references to +these transaction objects using request events that are emitted over the +lifecycle of a request. + +#### Requests with a body + +- `GuzzleHttp\Message\EntityEnclosingRequest` and + `GuzzleHttp\Message\EntityEnclosingRequestInterface` have been removed. The + separation between requests that contain a body and requests that do not + contain a body has been removed, and now `GuzzleHttp\Message\RequestInterface` + handles both use cases. +- Any method that previously accepts a `GuzzleHttp\Response` object now accept a + `GuzzleHttp\Message\ResponseInterface`. +- `GuzzleHttp\Message\RequestFactoryInterface` has been renamed to + `GuzzleHttp\Message\MessageFactoryInterface`. This interface is used to create + both requests and responses and is implemented in + `GuzzleHttp\Message\MessageFactory`. +- POST field and file methods have been removed from the request object. You + must now use the methods made available to `GuzzleHttp\Post\PostBodyInterface` + to control the format of a POST body. Requests that are created using a + standard `GuzzleHttp\Message\MessageFactoryInterface` will automatically use + a `GuzzleHttp\Post\PostBody` body if the body was passed as an array or if + the method is POST and no body is provided. + +```php +$request = $client->createRequest('POST', '/'); +$request->getBody()->setField('foo', 'bar'); +$request->getBody()->addFile(new PostFile('file_key', fopen('/path/to/content', 'r'))); +``` + +#### Headers + +- `GuzzleHttp\Message\Header` has been removed. Header values are now simply + represented by an array of values or as a string. Header values are returned + as a string by default when retrieving a header value from a message. You can + pass an optional argument of `true` to retrieve a header value as an array + of strings instead of a single concatenated string. +- `GuzzleHttp\PostFile` and `GuzzleHttp\PostFileInterface` have been moved to + `GuzzleHttp\Post`. This interface has been simplified and now allows the + addition of arbitrary headers. +- Custom headers like `GuzzleHttp\Message\Header\Link` have been removed. Most + of the custom headers are now handled separately in specific + subscribers/plugins, and `GuzzleHttp\Message\HeaderValues::parseParams()` has + been updated to properly handle headers that contain parameters (like the + `Link` header). + +#### Responses + +- `GuzzleHttp\Message\Response::getInfo()` and + `GuzzleHttp\Message\Response::setInfo()` have been removed. Use the event + system to retrieve this type of information. +- `GuzzleHttp\Message\Response::getRawHeaders()` has been removed. +- `GuzzleHttp\Message\Response::getMessage()` has been removed. +- `GuzzleHttp\Message\Response::calculateAge()` and other cache specific + methods have moved to the CacheSubscriber. +- Header specific helper functions like `getContentMd5()` have been removed. + Just use `getHeader('Content-MD5')` instead. +- `GuzzleHttp\Message\Response::setRequest()` and + `GuzzleHttp\Message\Response::getRequest()` have been removed. Use the event + system to work with request and response objects as a transaction. +- `GuzzleHttp\Message\Response::getRedirectCount()` has been removed. Use the + Redirect subscriber instead. +- `GuzzleHttp\Message\Response::isSuccessful()` and other related methods have + been removed. Use `getStatusCode()` instead. + +#### Streaming responses + +Streaming requests can now be created by a client directly, returning a +`GuzzleHttp\Message\ResponseInterface` object that contains a body stream +referencing an open PHP HTTP stream. + +```php +// 3.0 +use Guzzle\Stream\PhpStreamRequestFactory; +$request = $client->get('/'); +$factory = new PhpStreamRequestFactory(); +$stream = $factory->fromRequest($request); +$data = $stream->read(1024); + +// 4.0 +$response = $client->get('/', ['stream' => true]); +// Read some data off of the stream in the response body +$data = $response->getBody()->read(1024); +``` + +#### Redirects + +The `configureRedirects()` method has been removed in favor of a +`allow_redirects` request option. + +```php +// Standard redirects with a default of a max of 5 redirects +$request = $client->createRequest('GET', '/', ['allow_redirects' => true]); + +// Strict redirects with a custom number of redirects +$request = $client->createRequest('GET', '/', [ + 'allow_redirects' => ['max' => 5, 'strict' => true] +]); +``` + +#### EntityBody + +EntityBody interfaces and classes have been removed or moved to +`GuzzleHttp\Stream`. All classes and interfaces that once required +`GuzzleHttp\EntityBodyInterface` now require +`GuzzleHttp\Stream\StreamInterface`. Creating a new body for a request no +longer uses `GuzzleHttp\EntityBody::factory` but now uses +`GuzzleHttp\Stream\Stream::factory` or even better: +`GuzzleHttp\Stream\create()`. + +- `Guzzle\Http\EntityBodyInterface` is now `GuzzleHttp\Stream\StreamInterface` +- `Guzzle\Http\EntityBody` is now `GuzzleHttp\Stream\Stream` +- `Guzzle\Http\CachingEntityBody` is now `GuzzleHttp\Stream\CachingStream` +- `Guzzle\Http\ReadLimitEntityBody` is now `GuzzleHttp\Stream\LimitStream` +- `Guzzle\Http\IoEmittyinEntityBody` has been removed. + +#### Request lifecycle events + +Requests previously submitted a large number of requests. The number of events +emitted over the lifecycle of a request has been significantly reduced to make +it easier to understand how to extend the behavior of a request. All events +emitted during the lifecycle of a request now emit a custom +`GuzzleHttp\Event\EventInterface` object that contains context providing +methods and a way in which to modify the transaction at that specific point in +time (e.g., intercept the request and set a response on the transaction). + +- `request.before_send` has been renamed to `before` and now emits a + `GuzzleHttp\Event\BeforeEvent` +- `request.complete` has been renamed to `complete` and now emits a + `GuzzleHttp\Event\CompleteEvent`. +- `request.sent` has been removed. Use `complete`. +- `request.success` has been removed. Use `complete`. +- `error` is now an event that emits a `GuzzleHttp\Event\ErrorEvent`. +- `request.exception` has been removed. Use `error`. +- `request.receive.status_line` has been removed. +- `curl.callback.progress` has been removed. Use a custom `StreamInterface` to + maintain a status update. +- `curl.callback.write` has been removed. Use a custom `StreamInterface` to + intercept writes. +- `curl.callback.read` has been removed. Use a custom `StreamInterface` to + intercept reads. + +`headers` is a new event that is emitted after the response headers of a +request have been received before the body of the response is downloaded. This +event emits a `GuzzleHttp\Event\HeadersEvent`. + +You can intercept a request and inject a response using the `intercept()` event +of a `GuzzleHttp\Event\BeforeEvent`, `GuzzleHttp\Event\CompleteEvent`, and +`GuzzleHttp\Event\ErrorEvent` event. + +See: http://docs.guzzlephp.org/en/latest/events.html + +## Inflection + +The `Guzzle\Inflection` namespace has been removed. This is not a core concern +of Guzzle. + +## Iterator + +The `Guzzle\Iterator` namespace has been removed. + +- `Guzzle\Iterator\AppendIterator`, `Guzzle\Iterator\ChunkedIterator`, and + `Guzzle\Iterator\MethodProxyIterator` are nice, but not a core requirement of + Guzzle itself. +- `Guzzle\Iterator\FilterIterator` is no longer needed because an equivalent + class is shipped with PHP 5.4. +- `Guzzle\Iterator\MapIterator` is not really needed when using PHP 5.5 because + it's easier to just wrap an iterator in a generator that maps values. + +For a replacement of these iterators, see https://github.com/nikic/iter + +## Log + +The LogPlugin has moved to https://github.com/guzzle/log-subscriber. The +`Guzzle\Log` namespace has been removed. Guzzle now relies on +`Psr\Log\LoggerInterface` for all logging. The MessageFormatter class has been +moved to `GuzzleHttp\Subscriber\Log\Formatter`. + +## Parser + +The `Guzzle\Parser` namespace has been removed. This was previously used to +make it possible to plug in custom parsers for cookies, messages, URI +templates, and URLs; however, this level of complexity is not needed in Guzzle +so it has been removed. + +- Cookie: Cookie parsing logic has been moved to + `GuzzleHttp\Cookie\SetCookie::fromString`. +- Message: Message parsing logic for both requests and responses has been moved + to `GuzzleHttp\Message\MessageFactory::fromMessage`. Message parsing is only + used in debugging or deserializing messages, so it doesn't make sense for + Guzzle as a library to add this level of complexity to parsing messages. +- UriTemplate: URI template parsing has been moved to + `GuzzleHttp\UriTemplate`. The Guzzle library will automatically use the PECL + URI template library if it is installed. +- Url: URL parsing is now performed in `GuzzleHttp\Url::fromString` (previously + it was `Guzzle\Http\Url::factory()`). If custom URL parsing is necessary, + then developers are free to subclass `GuzzleHttp\Url`. + +## Plugin + +The `Guzzle\Plugin` namespace has been renamed to `GuzzleHttp\Subscriber`. +Several plugins are shipping with the core Guzzle library under this namespace. + +- `GuzzleHttp\Subscriber\Cookie`: Replaces the old CookiePlugin. Cookie jar + code has moved to `GuzzleHttp\Cookie`. +- `GuzzleHttp\Subscriber\History`: Replaces the old HistoryPlugin. +- `GuzzleHttp\Subscriber\HttpError`: Throws errors when a bad HTTP response is + received. +- `GuzzleHttp\Subscriber\Mock`: Replaces the old MockPlugin. +- `GuzzleHttp\Subscriber\Prepare`: Prepares the body of a request just before + sending. This subscriber is attached to all requests by default. +- `GuzzleHttp\Subscriber\Redirect`: Replaces the RedirectPlugin. + +The following plugins have been removed (third-parties are free to re-implement +these if needed): + +- `GuzzleHttp\Plugin\Async` has been removed. +- `GuzzleHttp\Plugin\CurlAuth` has been removed. +- `GuzzleHttp\Plugin\ErrorResponse\ErrorResponsePlugin` has been removed. This + functionality should instead be implemented with event listeners that occur + after normal response parsing occurs in the guzzle/command package. + +The following plugins are not part of the core Guzzle package, but are provided +in separate repositories: + +- `Guzzle\Http\Plugin\BackoffPlugin` has been rewritten to be much simpler + to build custom retry policies using simple functions rather than various + chained classes. See: https://github.com/guzzle/retry-subscriber +- `Guzzle\Http\Plugin\Cache\CachePlugin` has moved to + https://github.com/guzzle/cache-subscriber +- `Guzzle\Http\Plugin\Log\LogPlugin` has moved to + https://github.com/guzzle/log-subscriber +- `Guzzle\Http\Plugin\Md5\Md5Plugin` has moved to + https://github.com/guzzle/message-integrity-subscriber +- `Guzzle\Http\Plugin\Mock\MockPlugin` has moved to + `GuzzleHttp\Subscriber\MockSubscriber`. +- `Guzzle\Http\Plugin\Oauth\OauthPlugin` has moved to + https://github.com/guzzle/oauth-subscriber + +## Service + +The service description layer of Guzzle has moved into two separate packages: + +- http://github.com/guzzle/command Provides a high level abstraction over web + services by representing web service operations using commands. +- http://github.com/guzzle/guzzle-services Provides an implementation of + guzzle/command that provides request serialization and response parsing using + Guzzle service descriptions. + +## Stream + +Stream have moved to a separate package available at +https://github.com/guzzle/streams. + +`Guzzle\Stream\StreamInterface` has been given a large update to cleanly take +on the responsibilities of `Guzzle\Http\EntityBody` and +`Guzzle\Http\EntityBodyInterface` now that they have been removed. The number +of methods implemented by the `StreamInterface` has been drastically reduced to +allow developers to more easily extend and decorate stream behavior. + +## Removed methods from StreamInterface + +- `getStream` and `setStream` have been removed to better encapsulate streams. +- `getMetadata` and `setMetadata` have been removed in favor of + `GuzzleHttp\Stream\MetadataStreamInterface`. +- `getWrapper`, `getWrapperData`, `getStreamType`, and `getUri` have all been + removed. This data is accessible when + using streams that implement `GuzzleHttp\Stream\MetadataStreamInterface`. +- `rewind` has been removed. Use `seek(0)` for a similar behavior. + +## Renamed methods + +- `detachStream` has been renamed to `detach`. +- `feof` has been renamed to `eof`. +- `ftell` has been renamed to `tell`. +- `readLine` has moved from an instance method to a static class method of + `GuzzleHttp\Stream\Stream`. + +## Metadata streams + +`GuzzleHttp\Stream\MetadataStreamInterface` has been added to denote streams +that contain additional metadata accessible via `getMetadata()`. +`GuzzleHttp\Stream\StreamInterface::getMetadata` and +`GuzzleHttp\Stream\StreamInterface::setMetadata` have been removed. + +## StreamRequestFactory + +The entire concept of the StreamRequestFactory has been removed. The way this +was used in Guzzle 3 broke the actual interface of sending streaming requests +(instead of getting back a Response, you got a StreamInterface). Streaming +PHP requests are now implemented through the `GuzzleHttp\Adapter\StreamAdapter`. + +3.6 to 3.7 +---------- + +### Deprecations + +- You can now enable E_USER_DEPRECATED warnings to see if you are using any deprecated methods.: + +```php +\Guzzle\Common\Version::$emitWarnings = true; +``` + +The following APIs and options have been marked as deprecated: + +- Marked `Guzzle\Http\Message\Request::isResponseBodyRepeatable()` as deprecated. Use `$request->getResponseBody()->isRepeatable()` instead. +- Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. +- Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. +- Marked `Guzzle\Http\Message\Request::setIsRedirect()` as deprecated. Use the HistoryPlugin instead. +- Marked `Guzzle\Http\Message\Request::isRedirect()` as deprecated. Use the HistoryPlugin instead. +- Marked `Guzzle\Cache\CacheAdapterFactory::factory()` as deprecated +- Marked `Guzzle\Service\Client::enableMagicMethods()` as deprecated. Magic methods can no longer be disabled on a Guzzle\Service\Client. +- Marked `Guzzle\Parser\Url\UrlParser` as deprecated. Just use PHP's `parse_url()` and percent encode your UTF-8. +- Marked `Guzzle\Common\Collection::inject()` as deprecated. +- Marked `Guzzle\Plugin\CurlAuth\CurlAuthPlugin` as deprecated. Use + `$client->getConfig()->setPath('request.options/auth', array('user', 'pass', 'Basic|Digest|NTLM|Any'));` or + `$client->setDefaultOption('auth', array('user', 'pass', 'Basic|Digest|NTLM|Any'));` + +3.7 introduces `request.options` as a parameter for a client configuration and as an optional argument to all creational +request methods. When paired with a client's configuration settings, these options allow you to specify default settings +for various aspects of a request. Because these options make other previous configuration options redundant, several +configuration options and methods of a client and AbstractCommand have been deprecated. + +- Marked `Guzzle\Service\Client::getDefaultHeaders()` as deprecated. Use `$client->getDefaultOption('headers')`. +- Marked `Guzzle\Service\Client::setDefaultHeaders()` as deprecated. Use `$client->setDefaultOption('headers/{header_name}', 'value')`. +- Marked 'request.params' for `Guzzle\Http\Client` as deprecated. Use `$client->setDefaultOption('params/{param_name}', 'value')` +- Marked 'command.headers', 'command.response_body' and 'command.on_complete' as deprecated for AbstractCommand. These will work through Guzzle 4.0 + + $command = $client->getCommand('foo', array( + 'command.headers' => array('Test' => '123'), + 'command.response_body' => '/path/to/file' + )); + + // Should be changed to: + + $command = $client->getCommand('foo', array( + 'command.request_options' => array( + 'headers' => array('Test' => '123'), + 'save_as' => '/path/to/file' + ) + )); + +### Interface changes + +Additions and changes (you will need to update any implementations or subclasses you may have created): + +- Added an `$options` argument to the end of the following methods of `Guzzle\Http\ClientInterface`: + createRequest, head, delete, put, patch, post, options, prepareRequest +- Added an `$options` argument to the end of `Guzzle\Http\Message\Request\RequestFactoryInterface::createRequest()` +- Added an `applyOptions()` method to `Guzzle\Http\Message\Request\RequestFactoryInterface` +- Changed `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $body = null)` to + `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $options = array())`. You can still pass in a + resource, string, or EntityBody into the $options parameter to specify the download location of the response. +- Changed `Guzzle\Common\Collection::__construct($data)` to no longer accepts a null value for `$data` but a + default `array()` +- Added `Guzzle\Stream\StreamInterface::isRepeatable` +- Made `Guzzle\Http\Client::expandTemplate` and `getUriTemplate` protected methods. + +The following methods were removed from interfaces. All of these methods are still available in the concrete classes +that implement them, but you should update your code to use alternative methods: + +- Removed `Guzzle\Http\ClientInterface::setDefaultHeaders(). Use + `$client->getConfig()->setPath('request.options/headers/{header_name}', 'value')`. or + `$client->getConfig()->setPath('request.options/headers', array('header_name' => 'value'))` or + `$client->setDefaultOption('headers/{header_name}', 'value')`. or + `$client->setDefaultOption('headers', array('header_name' => 'value'))`. +- Removed `Guzzle\Http\ClientInterface::getDefaultHeaders(). Use `$client->getConfig()->getPath('request.options/headers')`. +- Removed `Guzzle\Http\ClientInterface::expandTemplate()`. This is an implementation detail. +- Removed `Guzzle\Http\ClientInterface::setRequestFactory()`. This is an implementation detail. +- Removed `Guzzle\Http\ClientInterface::getCurlMulti()`. This is a very specific implementation detail. +- Removed `Guzzle\Http\Message\RequestInterface::canCache`. Use the CachePlugin. +- Removed `Guzzle\Http\Message\RequestInterface::setIsRedirect`. Use the HistoryPlugin. +- Removed `Guzzle\Http\Message\RequestInterface::isRedirect`. Use the HistoryPlugin. + +### Cache plugin breaking changes + +- CacheKeyProviderInterface and DefaultCacheKeyProvider are no longer used. All of this logic is handled in a + CacheStorageInterface. These two objects and interface will be removed in a future version. +- Always setting X-cache headers on cached responses +- Default cache TTLs are now handled by the CacheStorageInterface of a CachePlugin +- `CacheStorageInterface::cache($key, Response $response, $ttl = null)` has changed to `cache(RequestInterface + $request, Response $response);` +- `CacheStorageInterface::fetch($key)` has changed to `fetch(RequestInterface $request);` +- `CacheStorageInterface::delete($key)` has changed to `delete(RequestInterface $request);` +- Added `CacheStorageInterface::purge($url)` +- `DefaultRevalidation::__construct(CacheKeyProviderInterface $cacheKey, CacheStorageInterface $cache, CachePlugin + $plugin)` has changed to `DefaultRevalidation::__construct(CacheStorageInterface $cache, + CanCacheStrategyInterface $canCache = null)` +- Added `RevalidationInterface::shouldRevalidate(RequestInterface $request, Response $response)` + +3.5 to 3.6 +---------- + +* Mixed casing of headers are now forced to be a single consistent casing across all values for that header. +* Messages internally use a HeaderCollection object to delegate handling case-insensitive header resolution +* Removed the whole changedHeader() function system of messages because all header changes now go through addHeader(). + For example, setHeader() first removes the header using unset on a HeaderCollection and then calls addHeader(). + Keeping the Host header and URL host in sync is now handled by overriding the addHeader method in Request. +* Specific header implementations can be created for complex headers. When a message creates a header, it uses a + HeaderFactory which can map specific headers to specific header classes. There is now a Link header and + CacheControl header implementation. +* Moved getLinks() from Response to just be used on a Link header object. + +If you previously relied on Guzzle\Http\Message\Header::raw(), then you will need to update your code to use the +HeaderInterface (e.g. toArray(), getAll(), etc.). + +### Interface changes + +* Removed from interface: Guzzle\Http\ClientInterface::setUriTemplate +* Removed from interface: Guzzle\Http\ClientInterface::setCurlMulti() +* Removed Guzzle\Http\Message\Request::receivedRequestHeader() and implemented this functionality in + Guzzle\Http\Curl\RequestMediator +* Removed the optional $asString parameter from MessageInterface::getHeader(). Just cast the header to a string. +* Removed the optional $tryChunkedTransfer option from Guzzle\Http\Message\EntityEnclosingRequestInterface +* Removed the $asObjects argument from Guzzle\Http\Message\MessageInterface::getHeaders() + +### Removed deprecated functions + +* Removed Guzzle\Parser\ParserRegister::get(). Use getParser() +* Removed Guzzle\Parser\ParserRegister::set(). Use registerParser(). + +### Deprecations + +* The ability to case-insensitively search for header values +* Guzzle\Http\Message\Header::hasExactHeader +* Guzzle\Http\Message\Header::raw. Use getAll() +* Deprecated cache control specific methods on Guzzle\Http\Message\AbstractMessage. Use the CacheControl header object + instead. + +### Other changes + +* All response header helper functions return a string rather than mixing Header objects and strings inconsistently +* Removed cURL blacklist support. This is no longer necessary now that Expect, Accept, etc. are managed by Guzzle + directly via interfaces +* Removed the injecting of a request object onto a response object. The methods to get and set a request still exist + but are a no-op until removed. +* Most classes that used to require a `Guzzle\Service\Command\CommandInterface` typehint now request a + `Guzzle\Service\Command\ArrayCommandInterface`. +* Added `Guzzle\Http\Message\RequestInterface::startResponse()` to the RequestInterface to handle injecting a response + on a request while the request is still being transferred +* `Guzzle\Service\Command\CommandInterface` now extends from ToArrayInterface and ArrayAccess + +3.3 to 3.4 +---------- + +Base URLs of a client now follow the rules of https://tools.ietf.org/html/rfc3986#section-5.2.2 when merging URLs. + +3.2 to 3.3 +---------- + +### Response::getEtag() quote stripping removed + +`Guzzle\Http\Message\Response::getEtag()` no longer strips quotes around the ETag response header + +### Removed `Guzzle\Http\Utils` + +The `Guzzle\Http\Utils` class was removed. This class was only used for testing. + +### Stream wrapper and type + +`Guzzle\Stream\Stream::getWrapper()` and `Guzzle\Stream\Stream::getStreamType()` are no longer converted to lowercase. + +### curl.emit_io became emit_io + +Emitting IO events from a RequestMediator is now a parameter that must be set in a request's curl options using the +'emit_io' key. This was previously set under a request's parameters using 'curl.emit_io' + +3.1 to 3.2 +---------- + +### CurlMulti is no longer reused globally + +Before 3.2, the same CurlMulti object was reused globally for each client. This can cause issue where plugins added +to a single client can pollute requests dispatched from other clients. + +If you still wish to reuse the same CurlMulti object with each client, then you can add a listener to the +ServiceBuilder's `service_builder.create_client` event to inject a custom CurlMulti object into each client as it is +created. + +```php +$multi = new Guzzle\Http\Curl\CurlMulti(); +$builder = Guzzle\Service\Builder\ServiceBuilder::factory('/path/to/config.json'); +$builder->addListener('service_builder.create_client', function ($event) use ($multi) { + $event['client']->setCurlMulti($multi); +} +}); +``` + +### No default path + +URLs no longer have a default path value of '/' if no path was specified. + +Before: + +```php +$request = $client->get('http://www.foo.com'); +echo $request->getUrl(); +// >> http://www.foo.com/ +``` + +After: + +```php +$request = $client->get('http://www.foo.com'); +echo $request->getUrl(); +// >> http://www.foo.com +``` + +### Less verbose BadResponseException + +The exception message for `Guzzle\Http\Exception\BadResponseException` no longer contains the full HTTP request and +response information. You can, however, get access to the request and response object by calling `getRequest()` or +`getResponse()` on the exception object. + +### Query parameter aggregation + +Multi-valued query parameters are no longer aggregated using a callback function. `Guzzle\Http\Query` now has a +setAggregator() method that accepts a `Guzzle\Http\QueryAggregator\QueryAggregatorInterface` object. This object is +responsible for handling the aggregation of multi-valued query string variables into a flattened hash. + +2.8 to 3.x +---------- + +### Guzzle\Service\Inspector + +Change `\Guzzle\Service\Inspector::fromConfig` to `\Guzzle\Common\Collection::fromConfig` + +**Before** + +```php +use Guzzle\Service\Inspector; + +class YourClient extends \Guzzle\Service\Client +{ + public static function factory($config = array()) + { + $default = array(); + $required = array('base_url', 'username', 'api_key'); + $config = Inspector::fromConfig($config, $default, $required); + + $client = new self( + $config->get('base_url'), + $config->get('username'), + $config->get('api_key') + ); + $client->setConfig($config); + + $client->setDescription(ServiceDescription::factory(__DIR__ . DIRECTORY_SEPARATOR . 'client.json')); + + return $client; + } +``` + +**After** + +```php +use Guzzle\Common\Collection; + +class YourClient extends \Guzzle\Service\Client +{ + public static function factory($config = array()) + { + $default = array(); + $required = array('base_url', 'username', 'api_key'); + $config = Collection::fromConfig($config, $default, $required); + + $client = new self( + $config->get('base_url'), + $config->get('username'), + $config->get('api_key') + ); + $client->setConfig($config); + + $client->setDescription(ServiceDescription::factory(__DIR__ . DIRECTORY_SEPARATOR . 'client.json')); + + return $client; + } +``` + +### Convert XML Service Descriptions to JSON + +**Before** + +```xml + + + + + + Get a list of groups + + + Uses a search query to get a list of groups + + + + Create a group + + + + + Delete a group by ID + + + + + + + Update a group + + + + + + +``` + +**After** + +```json +{ + "name": "Zendesk REST API v2", + "apiVersion": "2012-12-31", + "description":"Provides access to Zendesk views, groups, tickets, ticket fields, and users", + "operations": { + "list_groups": { + "httpMethod":"GET", + "uri": "groups.json", + "summary": "Get a list of groups" + }, + "search_groups":{ + "httpMethod":"GET", + "uri": "search.json?query=\"{query} type:group\"", + "summary": "Uses a search query to get a list of groups", + "parameters":{ + "query":{ + "location": "uri", + "description":"Zendesk Search Query", + "type": "string", + "required": true + } + } + }, + "create_group": { + "httpMethod":"POST", + "uri": "groups.json", + "summary": "Create a group", + "parameters":{ + "data": { + "type": "array", + "location": "body", + "description":"Group JSON", + "filters": "json_encode", + "required": true + }, + "Content-Type":{ + "type": "string", + "location":"header", + "static": "application/json" + } + } + }, + "delete_group": { + "httpMethod":"DELETE", + "uri": "groups/{id}.json", + "summary": "Delete a group", + "parameters":{ + "id":{ + "location": "uri", + "description":"Group to delete by ID", + "type": "integer", + "required": true + } + } + }, + "get_group": { + "httpMethod":"GET", + "uri": "groups/{id}.json", + "summary": "Get a ticket", + "parameters":{ + "id":{ + "location": "uri", + "description":"Group to get by ID", + "type": "integer", + "required": true + } + } + }, + "update_group": { + "httpMethod":"PUT", + "uri": "groups/{id}.json", + "summary": "Update a group", + "parameters":{ + "id": { + "location": "uri", + "description":"Group to update by ID", + "type": "integer", + "required": true + }, + "data": { + "type": "array", + "location": "body", + "description":"Group JSON", + "filters": "json_encode", + "required": true + }, + "Content-Type":{ + "type": "string", + "location":"header", + "static": "application/json" + } + } + } +} +``` + +### Guzzle\Service\Description\ServiceDescription + +Commands are now called Operations + +**Before** + +```php +use Guzzle\Service\Description\ServiceDescription; + +$sd = new ServiceDescription(); +$sd->getCommands(); // @returns ApiCommandInterface[] +$sd->hasCommand($name); +$sd->getCommand($name); // @returns ApiCommandInterface|null +$sd->addCommand($command); // @param ApiCommandInterface $command +``` + +**After** + +```php +use Guzzle\Service\Description\ServiceDescription; + +$sd = new ServiceDescription(); +$sd->getOperations(); // @returns OperationInterface[] +$sd->hasOperation($name); +$sd->getOperation($name); // @returns OperationInterface|null +$sd->addOperation($operation); // @param OperationInterface $operation +``` + +### Guzzle\Common\Inflection\Inflector + +Namespace is now `Guzzle\Inflection\Inflector` + +### Guzzle\Http\Plugin + +Namespace is now `Guzzle\Plugin`. Many other changes occur within this namespace and are detailed in their own sections below. + +### Guzzle\Http\Plugin\LogPlugin and Guzzle\Common\Log + +Now `Guzzle\Plugin\Log\LogPlugin` and `Guzzle\Log` respectively. + +**Before** + +```php +use Guzzle\Common\Log\ClosureLogAdapter; +use Guzzle\Http\Plugin\LogPlugin; + +/** @var \Guzzle\Http\Client */ +$client; + +// $verbosity is an integer indicating desired message verbosity level +$client->addSubscriber(new LogPlugin(new ClosureLogAdapter(function($m) { echo $m; }, $verbosity = LogPlugin::LOG_VERBOSE); +``` + +**After** + +```php +use Guzzle\Log\ClosureLogAdapter; +use Guzzle\Log\MessageFormatter; +use Guzzle\Plugin\Log\LogPlugin; + +/** @var \Guzzle\Http\Client */ +$client; + +// $format is a string indicating desired message format -- @see MessageFormatter +$client->addSubscriber(new LogPlugin(new ClosureLogAdapter(function($m) { echo $m; }, $format = MessageFormatter::DEBUG_FORMAT); +``` + +### Guzzle\Http\Plugin\CurlAuthPlugin + +Now `Guzzle\Plugin\CurlAuth\CurlAuthPlugin`. + +### Guzzle\Http\Plugin\ExponentialBackoffPlugin + +Now `Guzzle\Plugin\Backoff\BackoffPlugin`, and other changes. + +**Before** + +```php +use Guzzle\Http\Plugin\ExponentialBackoffPlugin; + +$backoffPlugin = new ExponentialBackoffPlugin($maxRetries, array_merge( + ExponentialBackoffPlugin::getDefaultFailureCodes(), array(429) + )); + +$client->addSubscriber($backoffPlugin); +``` + +**After** + +```php +use Guzzle\Plugin\Backoff\BackoffPlugin; +use Guzzle\Plugin\Backoff\HttpBackoffStrategy; + +// Use convenient factory method instead -- see implementation for ideas of what +// you can do with chaining backoff strategies +$backoffPlugin = BackoffPlugin::getExponentialBackoff($maxRetries, array_merge( + HttpBackoffStrategy::getDefaultFailureCodes(), array(429) + )); +$client->addSubscriber($backoffPlugin); +``` + +### Known Issues + +#### [BUG] Accept-Encoding header behavior changed unintentionally. + +(See #217) (Fixed in 09daeb8c666fb44499a0646d655a8ae36456575e) + +In version 2.8 setting the `Accept-Encoding` header would set the CURLOPT_ENCODING option, which permitted cURL to +properly handle gzip/deflate compressed responses from the server. In versions affected by this bug this does not happen. +See issue #217 for a workaround, or use a version containing the fix. diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/composer.json b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/composer.json new file mode 100644 index 0000000..5da35a5 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/composer.json @@ -0,0 +1,73 @@ +{ + "name": "guzzlehttp/guzzle", + "type": "library", + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "framework", + "http", + "rest", + "web service", + "curl", + "client", + "HTTP client", + "PSR-7", + "PSR-18" + ], + "homepage": "http://guzzlephp.org/", + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "require": { + "php": "^7.2.5 || ^8.0", + "ext-json": "*", + "guzzlehttp/promises": "^1.4", + "guzzlehttp/psr7": "^1.7 || ^2.0", + "psr/http-client": "^1.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "ext-curl": "*", + "bamarni/composer-bin-plugin": "^1.4.1", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "psr/log": "^1.1" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "config": { + "sort-packages": true + }, + "extra": { + "branch-alias": { + "dev-master": "7.3-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "autoload-dev": { + "psr-4": { + "GuzzleHttp\\Tests\\": "tests/" + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/BodySummarizer.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/BodySummarizer.php new file mode 100644 index 0000000..6eca94e --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/BodySummarizer.php @@ -0,0 +1,28 @@ +truncateAt = $truncateAt; + } + + /** + * Returns a summarized message body. + */ + public function summarize(MessageInterface $message): ?string + { + return $this->truncateAt === null + ? \GuzzleHttp\Psr7\Message::bodySummary($message) + : \GuzzleHttp\Psr7\Message::bodySummary($message, $this->truncateAt); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/BodySummarizerInterface.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/BodySummarizerInterface.php new file mode 100644 index 0000000..3e02e03 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/BodySummarizerInterface.php @@ -0,0 +1,13 @@ + 'http://www.foo.com/1.0/', + * 'timeout' => 0, + * 'allow_redirects' => false, + * 'proxy' => '192.168.16.1:10' + * ]); + * + * Client configuration settings include the following options: + * + * - handler: (callable) Function that transfers HTTP requests over the + * wire. The function is called with a Psr7\Http\Message\RequestInterface + * and array of transfer options, and must return a + * GuzzleHttp\Promise\PromiseInterface that is fulfilled with a + * Psr7\Http\Message\ResponseInterface on success. + * If no handler is provided, a default handler will be created + * that enables all of the request options below by attaching all of the + * default middleware to the handler. + * - base_uri: (string|UriInterface) Base URI of the client that is merged + * into relative URIs. Can be a string or instance of UriInterface. + * - **: any request option + * + * @param array $config Client configuration settings. + * + * @see \GuzzleHttp\RequestOptions for a list of available request options. + */ + public function __construct(array $config = []) + { + if (!isset($config['handler'])) { + $config['handler'] = HandlerStack::create(); + } elseif (!\is_callable($config['handler'])) { + throw new InvalidArgumentException('handler must be a callable'); + } + + // Convert the base_uri to a UriInterface + if (isset($config['base_uri'])) { + $config['base_uri'] = Psr7\Utils::uriFor($config['base_uri']); + } + + $this->configureDefaults($config); + } + + /** + * @param string $method + * @param array $args + * + * @return PromiseInterface|ResponseInterface + * + * @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0. + */ + public function __call($method, $args) + { + if (\count($args) < 1) { + throw new InvalidArgumentException('Magic request methods require a URI and optional options array'); + } + + $uri = $args[0]; + $opts = $args[1] ?? []; + + return \substr($method, -5) === 'Async' + ? $this->requestAsync(\substr($method, 0, -5), $uri, $opts) + : $this->request($method, $uri, $opts); + } + + /** + * Asynchronously send an HTTP request. + * + * @param array $options Request options to apply to the given + * request and to the transfer. See \GuzzleHttp\RequestOptions. + */ + public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface + { + // Merge the base URI into the request URI if needed. + $options = $this->prepareDefaults($options); + + return $this->transfer( + $request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')), + $options + ); + } + + /** + * Send an HTTP request. + * + * @param array $options Request options to apply to the given + * request and to the transfer. See \GuzzleHttp\RequestOptions. + * + * @throws GuzzleException + */ + public function send(RequestInterface $request, array $options = []): ResponseInterface + { + $options[RequestOptions::SYNCHRONOUS] = true; + return $this->sendAsync($request, $options)->wait(); + } + + /** + * The HttpClient PSR (PSR-18) specify this method. + * + * @inheritDoc + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + $options[RequestOptions::SYNCHRONOUS] = true; + $options[RequestOptions::ALLOW_REDIRECTS] = false; + $options[RequestOptions::HTTP_ERRORS] = false; + + return $this->sendAsync($request, $options)->wait(); + } + + /** + * Create and send an asynchronous HTTP request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string $method HTTP method + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. + */ + public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface + { + $options = $this->prepareDefaults($options); + // Remove request modifying parameter because it can be done up-front. + $headers = $options['headers'] ?? []; + $body = $options['body'] ?? null; + $version = $options['version'] ?? '1.1'; + // Merge the URI into the base URI. + $uri = $this->buildUri(Psr7\Utils::uriFor($uri), $options); + if (\is_array($body)) { + throw $this->invalidBody(); + } + $request = new Psr7\Request($method, $uri, $headers, $body, $version); + // Remove the option so that they are not doubly-applied. + unset($options['headers'], $options['body'], $options['version']); + + return $this->transfer($request, $options); + } + + /** + * Create and send an HTTP request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string $method HTTP method. + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. + * + * @throws GuzzleException + */ + public function request(string $method, $uri = '', array $options = []): ResponseInterface + { + $options[RequestOptions::SYNCHRONOUS] = true; + return $this->requestAsync($method, $uri, $options)->wait(); + } + + /** + * Get a client configuration option. + * + * These options include default request options of the client, a "handler" + * (if utilized by the concrete client), and a "base_uri" if utilized by + * the concrete client. + * + * @param string|null $option The config option to retrieve. + * + * @return mixed + * + * @deprecated Client::getConfig will be removed in guzzlehttp/guzzle:8.0. + */ + public function getConfig(?string $option = null) + { + return $option === null + ? $this->config + : ($this->config[$option] ?? null); + } + + private function buildUri(UriInterface $uri, array $config): UriInterface + { + if (isset($config['base_uri'])) { + $uri = Psr7\UriResolver::resolve(Psr7\Utils::uriFor($config['base_uri']), $uri); + } + + if (isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) { + $idnOptions = ($config['idn_conversion'] === true) ? \IDNA_DEFAULT : $config['idn_conversion']; + $uri = Utils::idnUriConvert($uri, $idnOptions); + } + + return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri; + } + + /** + * Configures the default options for a client. + */ + private function configureDefaults(array $config): void + { + $defaults = [ + 'allow_redirects' => RedirectMiddleware::$defaultSettings, + 'http_errors' => true, + 'decode_content' => true, + 'verify' => true, + 'cookies' => false, + 'idn_conversion' => false, + ]; + + // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set. + + // We can only trust the HTTP_PROXY environment variable in a CLI + // process due to the fact that PHP has no reliable mechanism to + // get environment variables that start with "HTTP_". + if (\PHP_SAPI === 'cli' && ($proxy = Utils::getenv('HTTP_PROXY'))) { + $defaults['proxy']['http'] = $proxy; + } + + if ($proxy = Utils::getenv('HTTPS_PROXY')) { + $defaults['proxy']['https'] = $proxy; + } + + if ($noProxy = Utils::getenv('NO_PROXY')) { + $cleanedNoProxy = \str_replace(' ', '', $noProxy); + $defaults['proxy']['no'] = \explode(',', $cleanedNoProxy); + } + + $this->config = $config + $defaults; + + if (!empty($config['cookies']) && $config['cookies'] === true) { + $this->config['cookies'] = new CookieJar(); + } + + // Add the default user-agent header. + if (!isset($this->config['headers'])) { + $this->config['headers'] = ['User-Agent' => Utils::defaultUserAgent()]; + } else { + // Add the User-Agent header if one was not already set. + foreach (\array_keys($this->config['headers']) as $name) { + if (\strtolower($name) === 'user-agent') { + return; + } + } + $this->config['headers']['User-Agent'] = Utils::defaultUserAgent(); + } + } + + /** + * Merges default options into the array. + * + * @param array $options Options to modify by reference + */ + private function prepareDefaults(array $options): array + { + $defaults = $this->config; + + if (!empty($defaults['headers'])) { + // Default headers are only added if they are not present. + $defaults['_conditional'] = $defaults['headers']; + unset($defaults['headers']); + } + + // Special handling for headers is required as they are added as + // conditional headers and as headers passed to a request ctor. + if (\array_key_exists('headers', $options)) { + // Allows default headers to be unset. + if ($options['headers'] === null) { + $defaults['_conditional'] = []; + unset($options['headers']); + } elseif (!\is_array($options['headers'])) { + throw new InvalidArgumentException('headers must be an array'); + } + } + + // Shallow merge defaults underneath options. + $result = $options + $defaults; + + // Remove null values. + foreach ($result as $k => $v) { + if ($v === null) { + unset($result[$k]); + } + } + + return $result; + } + + /** + * Transfers the given request and applies request options. + * + * The URI of the request is not modified and the request options are used + * as-is without merging in default options. + * + * @param array $options See \GuzzleHttp\RequestOptions. + */ + private function transfer(RequestInterface $request, array $options): PromiseInterface + { + $request = $this->applyOptions($request, $options); + /** @var HandlerStack $handler */ + $handler = $options['handler']; + + try { + return P\Create::promiseFor($handler($request, $options)); + } catch (\Exception $e) { + return P\Create::rejectionFor($e); + } + } + + /** + * Applies the array of request options to a request. + */ + private function applyOptions(RequestInterface $request, array &$options): RequestInterface + { + $modify = [ + 'set_headers' => [], + ]; + + if (isset($options['headers'])) { + $modify['set_headers'] = $options['headers']; + unset($options['headers']); + } + + if (isset($options['form_params'])) { + if (isset($options['multipart'])) { + throw new InvalidArgumentException('You cannot use ' + . 'form_params and multipart at the same time. Use the ' + . 'form_params option if you want to send application/' + . 'x-www-form-urlencoded requests, and the multipart ' + . 'option to send multipart/form-data requests.'); + } + $options['body'] = \http_build_query($options['form_params'], '', '&'); + unset($options['form_params']); + // Ensure that we don't have the header in different case and set the new value. + $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); + $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; + } + + if (isset($options['multipart'])) { + $options['body'] = new Psr7\MultipartStream($options['multipart']); + unset($options['multipart']); + } + + if (isset($options['json'])) { + $options['body'] = Utils::jsonEncode($options['json']); + unset($options['json']); + // Ensure that we don't have the header in different case and set the new value. + $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); + $options['_conditional']['Content-Type'] = 'application/json'; + } + + if (!empty($options['decode_content']) + && $options['decode_content'] !== true + ) { + // Ensure that we don't have the header in different case and set the new value. + $options['_conditional'] = Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']); + $modify['set_headers']['Accept-Encoding'] = $options['decode_content']; + } + + if (isset($options['body'])) { + if (\is_array($options['body'])) { + throw $this->invalidBody(); + } + $modify['body'] = Psr7\Utils::streamFor($options['body']); + unset($options['body']); + } + + if (!empty($options['auth']) && \is_array($options['auth'])) { + $value = $options['auth']; + $type = isset($value[2]) ? \strtolower($value[2]) : 'basic'; + switch ($type) { + case 'basic': + // Ensure that we don't have the header in different case and set the new value. + $modify['set_headers'] = Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']); + $modify['set_headers']['Authorization'] = 'Basic ' + . \base64_encode("$value[0]:$value[1]"); + break; + case 'digest': + // @todo: Do not rely on curl + $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST; + $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]"; + break; + case 'ntlm': + $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM; + $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]"; + break; + } + } + + if (isset($options['query'])) { + $value = $options['query']; + if (\is_array($value)) { + $value = \http_build_query($value, '', '&', \PHP_QUERY_RFC3986); + } + if (!\is_string($value)) { + throw new InvalidArgumentException('query must be a string or array'); + } + $modify['query'] = $value; + unset($options['query']); + } + + // Ensure that sink is not an invalid value. + if (isset($options['sink'])) { + // TODO: Add more sink validation? + if (\is_bool($options['sink'])) { + throw new InvalidArgumentException('sink must not be a boolean'); + } + } + + $request = Psr7\Utils::modifyRequest($request, $modify); + if ($request->getBody() instanceof Psr7\MultipartStream) { + // Use a multipart/form-data POST if a Content-Type is not set. + // Ensure that we don't have the header in different case and set the new value. + $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); + $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' + . $request->getBody()->getBoundary(); + } + + // Merge in conditional headers if they are not present. + if (isset($options['_conditional'])) { + // Build up the changes so it's in a single clone of the message. + $modify = []; + foreach ($options['_conditional'] as $k => $v) { + if (!$request->hasHeader($k)) { + $modify['set_headers'][$k] = $v; + } + } + $request = Psr7\Utils::modifyRequest($request, $modify); + // Don't pass this internal value along to middleware/handlers. + unset($options['_conditional']); + } + + return $request; + } + + /** + * Return an InvalidArgumentException with pre-set message. + */ + private function invalidBody(): InvalidArgumentException + { + return new InvalidArgumentException('Passing in the "body" request ' + . 'option as an array to send a request is not supported. ' + . 'Please use the "form_params" request option to send a ' + . 'application/x-www-form-urlencoded request, or the "multipart" ' + . 'request option to send a multipart/form-data request.'); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/ClientInterface.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/ClientInterface.php new file mode 100644 index 0000000..f257a1a --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/ClientInterface.php @@ -0,0 +1,84 @@ +request('GET', $uri, $options); + } + + /** + * Create and send an HTTP HEAD request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + * + * @throws GuzzleException + */ + public function head($uri, array $options = []): ResponseInterface + { + return $this->request('HEAD', $uri, $options); + } + + /** + * Create and send an HTTP PUT request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + * + * @throws GuzzleException + */ + public function put($uri, array $options = []): ResponseInterface + { + return $this->request('PUT', $uri, $options); + } + + /** + * Create and send an HTTP POST request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + * + * @throws GuzzleException + */ + public function post($uri, array $options = []): ResponseInterface + { + return $this->request('POST', $uri, $options); + } + + /** + * Create and send an HTTP PATCH request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + * + * @throws GuzzleException + */ + public function patch($uri, array $options = []): ResponseInterface + { + return $this->request('PATCH', $uri, $options); + } + + /** + * Create and send an HTTP DELETE request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + * + * @throws GuzzleException + */ + public function delete($uri, array $options = []): ResponseInterface + { + return $this->request('DELETE', $uri, $options); + } + + /** + * Create and send an asynchronous HTTP request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string $method HTTP method + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + abstract public function requestAsync(string $method, $uri, array $options = []): PromiseInterface; + + /** + * Create and send an asynchronous HTTP GET request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + public function getAsync($uri, array $options = []): PromiseInterface + { + return $this->requestAsync('GET', $uri, $options); + } + + /** + * Create and send an asynchronous HTTP HEAD request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + public function headAsync($uri, array $options = []): PromiseInterface + { + return $this->requestAsync('HEAD', $uri, $options); + } + + /** + * Create and send an asynchronous HTTP PUT request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + public function putAsync($uri, array $options = []): PromiseInterface + { + return $this->requestAsync('PUT', $uri, $options); + } + + /** + * Create and send an asynchronous HTTP POST request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + public function postAsync($uri, array $options = []): PromiseInterface + { + return $this->requestAsync('POST', $uri, $options); + } + + /** + * Create and send an asynchronous HTTP PATCH request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + public function patchAsync($uri, array $options = []): PromiseInterface + { + return $this->requestAsync('PATCH', $uri, $options); + } + + /** + * Create and send an asynchronous HTTP DELETE request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + public function deleteAsync($uri, array $options = []): PromiseInterface + { + return $this->requestAsync('DELETE', $uri, $options); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php new file mode 100644 index 0000000..d6757c6 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php @@ -0,0 +1,313 @@ +strictMode = $strictMode; + + foreach ($cookieArray as $cookie) { + if (!($cookie instanceof SetCookie)) { + $cookie = new SetCookie($cookie); + } + $this->setCookie($cookie); + } + } + + /** + * Create a new Cookie jar from an associative array and domain. + * + * @param array $cookies Cookies to create the jar from + * @param string $domain Domain to set the cookies to + */ + public static function fromArray(array $cookies, string $domain): self + { + $cookieJar = new self(); + foreach ($cookies as $name => $value) { + $cookieJar->setCookie(new SetCookie([ + 'Domain' => $domain, + 'Name' => $name, + 'Value' => $value, + 'Discard' => true + ])); + } + + return $cookieJar; + } + + /** + * Evaluate if this cookie should be persisted to storage + * that survives between requests. + * + * @param SetCookie $cookie Being evaluated. + * @param bool $allowSessionCookies If we should persist session cookies + */ + public static function shouldPersist(SetCookie $cookie, bool $allowSessionCookies = false): bool + { + if ($cookie->getExpires() || $allowSessionCookies) { + if (!$cookie->getDiscard()) { + return true; + } + } + + return false; + } + + /** + * Finds and returns the cookie based on the name + * + * @param string $name cookie name to search for + * + * @return SetCookie|null cookie that was found or null if not found + */ + public function getCookieByName(string $name): ?SetCookie + { + foreach ($this->cookies as $cookie) { + if ($cookie->getName() !== null && \strcasecmp($cookie->getName(), $name) === 0) { + return $cookie; + } + } + + return null; + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return \array_map(static function (SetCookie $cookie): array { + return $cookie->toArray(); + }, $this->getIterator()->getArrayCopy()); + } + + /** + * @inheritDoc + */ + public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void + { + if (!$domain) { + $this->cookies = []; + return; + } elseif (!$path) { + $this->cookies = \array_filter( + $this->cookies, + static function (SetCookie $cookie) use ($domain): bool { + return !$cookie->matchesDomain($domain); + } + ); + } elseif (!$name) { + $this->cookies = \array_filter( + $this->cookies, + static function (SetCookie $cookie) use ($path, $domain): bool { + return !($cookie->matchesPath($path) && + $cookie->matchesDomain($domain)); + } + ); + } else { + $this->cookies = \array_filter( + $this->cookies, + static function (SetCookie $cookie) use ($path, $domain, $name) { + return !($cookie->getName() == $name && + $cookie->matchesPath($path) && + $cookie->matchesDomain($domain)); + } + ); + } + } + + /** + * @inheritDoc + */ + public function clearSessionCookies(): void + { + $this->cookies = \array_filter( + $this->cookies, + static function (SetCookie $cookie): bool { + return !$cookie->getDiscard() && $cookie->getExpires(); + } + ); + } + + /** + * @inheritDoc + */ + public function setCookie(SetCookie $cookie): bool + { + // If the name string is empty (but not 0), ignore the set-cookie + // string entirely. + $name = $cookie->getName(); + if (!$name && $name !== '0') { + return false; + } + + // Only allow cookies with set and valid domain, name, value + $result = $cookie->validate(); + if ($result !== true) { + if ($this->strictMode) { + throw new \RuntimeException('Invalid cookie: ' . $result); + } + $this->removeCookieIfEmpty($cookie); + return false; + } + + // Resolve conflicts with previously set cookies + foreach ($this->cookies as $i => $c) { + + // Two cookies are identical, when their path, and domain are + // identical. + if ($c->getPath() != $cookie->getPath() || + $c->getDomain() != $cookie->getDomain() || + $c->getName() != $cookie->getName() + ) { + continue; + } + + // The previously set cookie is a discard cookie and this one is + // not so allow the new cookie to be set + if (!$cookie->getDiscard() && $c->getDiscard()) { + unset($this->cookies[$i]); + continue; + } + + // If the new cookie's expiration is further into the future, then + // replace the old cookie + if ($cookie->getExpires() > $c->getExpires()) { + unset($this->cookies[$i]); + continue; + } + + // If the value has changed, we better change it + if ($cookie->getValue() !== $c->getValue()) { + unset($this->cookies[$i]); + continue; + } + + // The cookie exists, so no need to continue + return false; + } + + $this->cookies[] = $cookie; + + return true; + } + + public function count(): int + { + return \count($this->cookies); + } + + /** + * @return \ArrayIterator + */ + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator(\array_values($this->cookies)); + } + + public function extractCookies(RequestInterface $request, ResponseInterface $response): void + { + if ($cookieHeader = $response->getHeader('Set-Cookie')) { + foreach ($cookieHeader as $cookie) { + $sc = SetCookie::fromString($cookie); + if (!$sc->getDomain()) { + $sc->setDomain($request->getUri()->getHost()); + } + if (0 !== \strpos($sc->getPath(), '/')) { + $sc->setPath($this->getCookiePathFromRequest($request)); + } + $this->setCookie($sc); + } + } + } + + /** + * Computes cookie path following RFC 6265 section 5.1.4 + * + * @link https://tools.ietf.org/html/rfc6265#section-5.1.4 + */ + private function getCookiePathFromRequest(RequestInterface $request): string + { + $uriPath = $request->getUri()->getPath(); + if ('' === $uriPath) { + return '/'; + } + if (0 !== \strpos($uriPath, '/')) { + return '/'; + } + if ('/' === $uriPath) { + return '/'; + } + $lastSlashPos = \strrpos($uriPath, '/'); + if (0 === $lastSlashPos || false === $lastSlashPos) { + return '/'; + } + + return \substr($uriPath, 0, $lastSlashPos); + } + + public function withCookieHeader(RequestInterface $request): RequestInterface + { + $values = []; + $uri = $request->getUri(); + $scheme = $uri->getScheme(); + $host = $uri->getHost(); + $path = $uri->getPath() ?: '/'; + + foreach ($this->cookies as $cookie) { + if ($cookie->matchesPath($path) && + $cookie->matchesDomain($host) && + !$cookie->isExpired() && + (!$cookie->getSecure() || $scheme === 'https') + ) { + $values[] = $cookie->getName() . '=' + . $cookie->getValue(); + } + } + + return $values + ? $request->withHeader('Cookie', \implode('; ', $values)) + : $request; + } + + /** + * If a cookie already exists and the server asks to set it again with a + * null value, the cookie must be deleted. + */ + private function removeCookieIfEmpty(SetCookie $cookie): void + { + $cookieValue = $cookie->getValue(); + if ($cookieValue === null || $cookieValue === '') { + $this->clear( + $cookie->getDomain(), + $cookie->getPath(), + $cookie->getName() + ); + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php new file mode 100644 index 0000000..7df374b --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php @@ -0,0 +1,79 @@ + + */ +interface CookieJarInterface extends \Countable, \IteratorAggregate +{ + /** + * Create a request with added cookie headers. + * + * If no matching cookies are found in the cookie jar, then no Cookie + * header is added to the request and the same request is returned. + * + * @param RequestInterface $request Request object to modify. + * + * @return RequestInterface returns the modified request. + */ + public function withCookieHeader(RequestInterface $request): RequestInterface; + + /** + * Extract cookies from an HTTP response and store them in the CookieJar. + * + * @param RequestInterface $request Request that was sent + * @param ResponseInterface $response Response that was received + */ + public function extractCookies(RequestInterface $request, ResponseInterface $response): void; + + /** + * Sets a cookie in the cookie jar. + * + * @param SetCookie $cookie Cookie to set. + * + * @return bool Returns true on success or false on failure + */ + public function setCookie(SetCookie $cookie): bool; + + /** + * Remove cookies currently held in the cookie jar. + * + * Invoking this method without arguments will empty the whole cookie jar. + * If given a $domain argument only cookies belonging to that domain will + * be removed. If given a $domain and $path argument, cookies belonging to + * the specified path within that domain are removed. If given all three + * arguments, then the cookie with the specified name, path and domain is + * removed. + * + * @param string|null $domain Clears cookies matching a domain + * @param string|null $path Clears cookies matching a domain and path + * @param string|null $name Clears cookies matching a domain, path, and name + */ + public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void; + + /** + * Discard all sessions cookies. + * + * Removes cookies that don't have an expire field or a have a discard + * field set to true. To be called when the user agent shuts down according + * to RFC 2965. + */ + public function clearSessionCookies(): void; + + /** + * Converts the cookie jar to an array. + */ + public function toArray(): array; +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php new file mode 100644 index 0000000..290236d --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php @@ -0,0 +1,101 @@ +filename = $cookieFile; + $this->storeSessionCookies = $storeSessionCookies; + + if (\file_exists($cookieFile)) { + $this->load($cookieFile); + } + } + + /** + * Saves the file when shutting down + */ + public function __destruct() + { + $this->save($this->filename); + } + + /** + * Saves the cookies to a file. + * + * @param string $filename File to save + * + * @throws \RuntimeException if the file cannot be found or created + */ + public function save(string $filename): void + { + $json = []; + /** @var SetCookie $cookie */ + foreach ($this as $cookie) { + if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { + $json[] = $cookie->toArray(); + } + } + + $jsonStr = Utils::jsonEncode($json); + if (false === \file_put_contents($filename, $jsonStr, \LOCK_EX)) { + throw new \RuntimeException("Unable to save file {$filename}"); + } + } + + /** + * Load cookies from a JSON formatted file. + * + * Old cookies are kept unless overwritten by newly loaded ones. + * + * @param string $filename Cookie file to load. + * + * @throws \RuntimeException if the file cannot be loaded. + */ + public function load(string $filename): void + { + $json = \file_get_contents($filename); + if (false === $json) { + throw new \RuntimeException("Unable to load file {$filename}"); + } + if ($json === '') { + return; + } + + $data = Utils::jsonDecode($json, true); + if (\is_array($data)) { + foreach ($data as $cookie) { + $this->setCookie(new SetCookie($cookie)); + } + } elseif (\is_scalar($data) && !empty($data)) { + throw new \RuntimeException("Invalid cookie file: {$filename}"); + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php new file mode 100644 index 0000000..5d51ca9 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php @@ -0,0 +1,77 @@ +sessionKey = $sessionKey; + $this->storeSessionCookies = $storeSessionCookies; + $this->load(); + } + + /** + * Saves cookies to session when shutting down + */ + public function __destruct() + { + $this->save(); + } + + /** + * Save cookies to the client session + */ + public function save(): void + { + $json = []; + /** @var SetCookie $cookie */ + foreach ($this as $cookie) { + if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { + $json[] = $cookie->toArray(); + } + } + + $_SESSION[$this->sessionKey] = \json_encode($json); + } + + /** + * Load the contents of the client session into the data array + */ + protected function load(): void + { + if (!isset($_SESSION[$this->sessionKey])) { + return; + } + $data = \json_decode($_SESSION[$this->sessionKey], true); + if (\is_array($data)) { + foreach ($data as $cookie) { + $this->setCookie(new SetCookie($cookie)); + } + } elseif (\strlen($data)) { + throw new \RuntimeException("Invalid cookie data"); + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php new file mode 100644 index 0000000..602370d --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php @@ -0,0 +1,410 @@ + null, + 'Value' => null, + 'Domain' => null, + 'Path' => '/', + 'Max-Age' => null, + 'Expires' => null, + 'Secure' => false, + 'Discard' => false, + 'HttpOnly' => false + ]; + + /** + * @var array Cookie data + */ + private $data; + + /** + * Create a new SetCookie object from a string. + * + * @param string $cookie Set-Cookie header string + */ + public static function fromString(string $cookie): self + { + // Create the default return array + $data = self::$defaults; + // Explode the cookie string using a series of semicolons + $pieces = \array_filter(\array_map('trim', \explode(';', $cookie))); + // The name of the cookie (first kvp) must exist and include an equal sign. + if (!isset($pieces[0]) || \strpos($pieces[0], '=') === false) { + return new self($data); + } + + // Add the cookie pieces into the parsed data array + foreach ($pieces as $part) { + $cookieParts = \explode('=', $part, 2); + $key = \trim($cookieParts[0]); + $value = isset($cookieParts[1]) + ? \trim($cookieParts[1], " \n\r\t\0\x0B") + : true; + + // Only check for non-cookies when cookies have been found + if (!isset($data['Name'])) { + $data['Name'] = $key; + $data['Value'] = $value; + } else { + foreach (\array_keys(self::$defaults) as $search) { + if (!\strcasecmp($search, $key)) { + $data[$search] = $value; + continue 2; + } + } + $data[$key] = $value; + } + } + + return new self($data); + } + + /** + * @param array $data Array of cookie data provided by a Cookie parser + */ + public function __construct(array $data = []) + { + /** @var array|null $replaced will be null in case of replace error */ + $replaced = \array_replace(self::$defaults, $data); + if ($replaced === null) { + throw new \InvalidArgumentException('Unable to replace the default values for the Cookie.'); + } + + $this->data = $replaced; + // Extract the Expires value and turn it into a UNIX timestamp if needed + if (!$this->getExpires() && $this->getMaxAge()) { + // Calculate the Expires date + $this->setExpires(\time() + $this->getMaxAge()); + } elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) { + $this->setExpires($expires); + } + } + + public function __toString() + { + $str = $this->data['Name'] . '=' . $this->data['Value'] . '; '; + foreach ($this->data as $k => $v) { + if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) { + if ($k === 'Expires') { + $str .= 'Expires=' . \gmdate('D, d M Y H:i:s \G\M\T', $v) . '; '; + } else { + $str .= ($v === true ? $k : "{$k}={$v}") . '; '; + } + } + } + + return \rtrim($str, '; '); + } + + public function toArray(): array + { + return $this->data; + } + + /** + * Get the cookie name. + * + * @return string + */ + public function getName() + { + return $this->data['Name']; + } + + /** + * Set the cookie name. + * + * @param string $name Cookie name + */ + public function setName($name): void + { + $this->data['Name'] = $name; + } + + /** + * Get the cookie value. + * + * @return string|null + */ + public function getValue() + { + return $this->data['Value']; + } + + /** + * Set the cookie value. + * + * @param string $value Cookie value + */ + public function setValue($value): void + { + $this->data['Value'] = $value; + } + + /** + * Get the domain. + * + * @return string|null + */ + public function getDomain() + { + return $this->data['Domain']; + } + + /** + * Set the domain of the cookie. + * + * @param string $domain + */ + public function setDomain($domain): void + { + $this->data['Domain'] = $domain; + } + + /** + * Get the path. + * + * @return string + */ + public function getPath() + { + return $this->data['Path']; + } + + /** + * Set the path of the cookie. + * + * @param string $path Path of the cookie + */ + public function setPath($path): void + { + $this->data['Path'] = $path; + } + + /** + * Maximum lifetime of the cookie in seconds. + * + * @return int|null + */ + public function getMaxAge() + { + return $this->data['Max-Age']; + } + + /** + * Set the max-age of the cookie. + * + * @param int $maxAge Max age of the cookie in seconds + */ + public function setMaxAge($maxAge): void + { + $this->data['Max-Age'] = $maxAge; + } + + /** + * The UNIX timestamp when the cookie Expires. + * + * @return string|int|null + */ + public function getExpires() + { + return $this->data['Expires']; + } + + /** + * Set the unix timestamp for which the cookie will expire. + * + * @param int|string $timestamp Unix timestamp or any English textual datetime description. + */ + public function setExpires($timestamp): void + { + $this->data['Expires'] = \is_numeric($timestamp) + ? (int) $timestamp + : \strtotime($timestamp); + } + + /** + * Get whether or not this is a secure cookie. + * + * @return bool|null + */ + public function getSecure() + { + return $this->data['Secure']; + } + + /** + * Set whether or not the cookie is secure. + * + * @param bool $secure Set to true or false if secure + */ + public function setSecure($secure): void + { + $this->data['Secure'] = $secure; + } + + /** + * Get whether or not this is a session cookie. + * + * @return bool|null + */ + public function getDiscard() + { + return $this->data['Discard']; + } + + /** + * Set whether or not this is a session cookie. + * + * @param bool $discard Set to true or false if this is a session cookie + */ + public function setDiscard($discard): void + { + $this->data['Discard'] = $discard; + } + + /** + * Get whether or not this is an HTTP only cookie. + * + * @return bool + */ + public function getHttpOnly() + { + return $this->data['HttpOnly']; + } + + /** + * Set whether or not this is an HTTP only cookie. + * + * @param bool $httpOnly Set to true or false if this is HTTP only + */ + public function setHttpOnly($httpOnly): void + { + $this->data['HttpOnly'] = $httpOnly; + } + + /** + * Check if the cookie matches a path value. + * + * A request-path path-matches a given cookie-path if at least one of + * the following conditions holds: + * + * - The cookie-path and the request-path are identical. + * - The cookie-path is a prefix of the request-path, and the last + * character of the cookie-path is %x2F ("/"). + * - The cookie-path is a prefix of the request-path, and the first + * character of the request-path that is not included in the cookie- + * path is a %x2F ("/") character. + * + * @param string $requestPath Path to check against + */ + public function matchesPath(string $requestPath): bool + { + $cookiePath = $this->getPath(); + + // Match on exact matches or when path is the default empty "/" + if ($cookiePath === '/' || $cookiePath == $requestPath) { + return true; + } + + // Ensure that the cookie-path is a prefix of the request path. + if (0 !== \strpos($requestPath, $cookiePath)) { + return false; + } + + // Match if the last character of the cookie-path is "/" + if (\substr($cookiePath, -1, 1) === '/') { + return true; + } + + // Match if the first character not included in cookie path is "/" + return \substr($requestPath, \strlen($cookiePath), 1) === '/'; + } + + /** + * Check if the cookie matches a domain value. + * + * @param string $domain Domain to check against + */ + public function matchesDomain(string $domain): bool + { + $cookieDomain = $this->getDomain(); + if (null === $cookieDomain) { + return true; + } + + // Remove the leading '.' as per spec in RFC 6265. + // https://tools.ietf.org/html/rfc6265#section-5.2.3 + $cookieDomain = \ltrim($cookieDomain, '.'); + + // Domain not set or exact match. + if (!$cookieDomain || !\strcasecmp($domain, $cookieDomain)) { + return true; + } + + // Matching the subdomain according to RFC 6265. + // https://tools.ietf.org/html/rfc6265#section-5.1.3 + if (\filter_var($domain, \FILTER_VALIDATE_IP)) { + return false; + } + + return (bool) \preg_match('/\.' . \preg_quote($cookieDomain, '/') . '$/', $domain); + } + + /** + * Check if the cookie is expired. + */ + public function isExpired(): bool + { + return $this->getExpires() !== null && \time() > $this->getExpires(); + } + + /** + * Check if the cookie is valid according to RFC 6265. + * + * @return bool|string Returns true if valid or an error message if invalid + */ + public function validate() + { + $name = $this->getName(); + if ($name === '') { + return 'The cookie name must not be empty'; + } + + // Check if any of the invalid characters are present in the cookie name + if (\preg_match( + '/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', + $name + )) { + return 'Cookie name must not contain invalid characters: ASCII ' + . 'Control characters (0-31;127), space, tab and the ' + . 'following characters: ()<>@,;:\"/?={}'; + } + + // Value must not be null. 0 and empty string are valid. Empty strings + // are technically against RFC 6265, but known to happen in the wild. + $value = $this->getValue(); + if ($value === null) { + return 'The cookie value must not be empty'; + } + + // Domains must not be empty, but can be 0. "0" is not a valid internet + // domain, but may be used as server name in a private network. + $domain = $this->getDomain(); + if ($domain === null || $domain === '') { + return 'The cookie domain must not be empty'; + } + + return true; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php new file mode 100644 index 0000000..a80956c --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php @@ -0,0 +1,39 @@ +request = $request; + $this->handlerContext = $handlerContext; + } + + /** + * Get the request that caused the exception + */ + public function getRequest(): RequestInterface + { + return $this->request; + } + + /** + * Get contextual information about the error from the underlying handler. + * + * The contents of this array will vary depending on which handler you are + * using. It may also be just an empty array. Relying on this data will + * couple you to a specific handler, but can give more debug information + * when needed. + */ + public function getHandlerContext(): array + { + return $this->handlerContext; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/GuzzleException.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/GuzzleException.php new file mode 100644 index 0000000..fa3ed69 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/GuzzleException.php @@ -0,0 +1,9 @@ +getStatusCode() : 0; + parent::__construct($message, $code, $previous); + $this->request = $request; + $this->response = $response; + $this->handlerContext = $handlerContext; + } + + /** + * Wrap non-RequestExceptions with a RequestException + */ + public static function wrapException(RequestInterface $request, \Throwable $e): RequestException + { + return $e instanceof RequestException ? $e : new RequestException($e->getMessage(), $request, null, $e); + } + + /** + * Factory method to create a new exception with a normalized error message + * + * @param RequestInterface $request Request sent + * @param ResponseInterface $response Response received + * @param \Throwable|null $previous Previous exception + * @param array $handlerContext Optional handler context + * @param BodySummarizerInterface|null $bodySummarizer Optional body summarizer + */ + public static function create( + RequestInterface $request, + ResponseInterface $response = null, + \Throwable $previous = null, + array $handlerContext = [], + BodySummarizerInterface $bodySummarizer = null + ): self { + if (!$response) { + return new self( + 'Error completing request', + $request, + null, + $previous, + $handlerContext + ); + } + + $level = (int) \floor($response->getStatusCode() / 100); + if ($level === 4) { + $label = 'Client error'; + $className = ClientException::class; + } elseif ($level === 5) { + $label = 'Server error'; + $className = ServerException::class; + } else { + $label = 'Unsuccessful request'; + $className = __CLASS__; + } + + $uri = $request->getUri(); + $uri = static::obfuscateUri($uri); + + // Client Error: `GET /` resulted in a `404 Not Found` response: + // ... (truncated) + $message = \sprintf( + '%s: `%s %s` resulted in a `%s %s` response', + $label, + $request->getMethod(), + $uri, + $response->getStatusCode(), + $response->getReasonPhrase() + ); + + $summary = ($bodySummarizer ?? new BodySummarizer())->summarize($response); + + if ($summary !== null) { + $message .= ":\n{$summary}\n"; + } + + return new $className($message, $request, $response, $previous, $handlerContext); + } + + /** + * Obfuscates URI if there is a username and a password present + */ + private static function obfuscateUri(UriInterface $uri): UriInterface + { + $userInfo = $uri->getUserInfo(); + + if (false !== ($pos = \strpos($userInfo, ':'))) { + return $uri->withUserInfo(\substr($userInfo, 0, $pos), '***'); + } + + return $uri; + } + + /** + * Get the request that caused the exception + */ + public function getRequest(): RequestInterface + { + return $this->request; + } + + /** + * Get the associated response + */ + public function getResponse(): ?ResponseInterface + { + return $this->response; + } + + /** + * Check if a response was received + */ + public function hasResponse(): bool + { + return $this->response !== null; + } + + /** + * Get contextual information about the error from the underlying handler. + * + * The contents of this array will vary depending on which handler you are + * using. It may also be just an empty array. Relying on this data will + * couple you to a specific handler, but can give more debug information + * when needed. + */ + public function getHandlerContext(): array + { + return $this->handlerContext; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/ServerException.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/ServerException.php new file mode 100644 index 0000000..8055e06 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Exception/ServerException.php @@ -0,0 +1,10 @@ +maxHandles = $maxHandles; + } + + public function create(RequestInterface $request, array $options): EasyHandle + { + if (isset($options['curl']['body_as_string'])) { + $options['_body_as_string'] = $options['curl']['body_as_string']; + unset($options['curl']['body_as_string']); + } + + $easy = new EasyHandle; + $easy->request = $request; + $easy->options = $options; + $conf = $this->getDefaultConf($easy); + $this->applyMethod($easy, $conf); + $this->applyHandlerOptions($easy, $conf); + $this->applyHeaders($easy, $conf); + unset($conf['_headers']); + + // Add handler options from the request configuration options + if (isset($options['curl'])) { + $conf = \array_replace($conf, $options['curl']); + } + + $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy); + $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init(); + curl_setopt_array($easy->handle, $conf); + + return $easy; + } + + public function release(EasyHandle $easy): void + { + $resource = $easy->handle; + unset($easy->handle); + + if (\count($this->handles) >= $this->maxHandles) { + \curl_close($resource); + } else { + // Remove all callback functions as they can hold onto references + // and are not cleaned up by curl_reset. Using curl_setopt_array + // does not work for some reason, so removing each one + // individually. + \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null); + \curl_setopt($resource, \CURLOPT_READFUNCTION, null); + \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null); + \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null); + \curl_reset($resource); + $this->handles[] = $resource; + } + } + + /** + * Completes a cURL transaction, either returning a response promise or a + * rejected promise. + * + * @param callable(RequestInterface, array): PromiseInterface $handler + * @param CurlFactoryInterface $factory Dictates how the handle is released + */ + public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface + { + if (isset($easy->options['on_stats'])) { + self::invokeStats($easy); + } + + if (!$easy->response || $easy->errno) { + return self::finishError($handler, $easy, $factory); + } + + // Return the response if it is present and there is no error. + $factory->release($easy); + + // Rewind the body of the response if possible. + $body = $easy->response->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + + return new FulfilledPromise($easy->response); + } + + private static function invokeStats(EasyHandle $easy): void + { + $curlStats = \curl_getinfo($easy->handle); + $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME); + $stats = new TransferStats( + $easy->request, + $easy->response, + $curlStats['total_time'], + $easy->errno, + $curlStats + ); + ($easy->options['on_stats'])($stats); + } + + /** + * @param callable(RequestInterface, array): PromiseInterface $handler + */ + private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface + { + // Get error information and release the handle to the factory. + $ctx = [ + 'errno' => $easy->errno, + 'error' => \curl_error($easy->handle), + 'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME), + ] + \curl_getinfo($easy->handle); + $ctx[self::CURL_VERSION_STR] = \curl_version()['version']; + $factory->release($easy); + + // Retry when nothing is present or when curl failed to rewind. + if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) { + return self::retryFailedRewind($handler, $easy, $ctx); + } + + return self::createRejection($easy, $ctx); + } + + private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface + { + static $connectionErrors = [ + \CURLE_OPERATION_TIMEOUTED => true, + \CURLE_COULDNT_RESOLVE_HOST => true, + \CURLE_COULDNT_CONNECT => true, + \CURLE_SSL_CONNECT_ERROR => true, + \CURLE_GOT_NOTHING => true, + ]; + + if ($easy->createResponseException) { + return P\Create::rejectionFor( + new RequestException( + 'An error was encountered while creating the response', + $easy->request, + $easy->response, + $easy->createResponseException, + $ctx + ) + ); + } + + // If an exception was encountered during the onHeaders event, then + // return a rejected promise that wraps that exception. + if ($easy->onHeadersException) { + return P\Create::rejectionFor( + new RequestException( + 'An error was encountered during the on_headers event', + $easy->request, + $easy->response, + $easy->onHeadersException, + $ctx + ) + ); + } + + $message = \sprintf( + 'cURL error %s: %s (%s)', + $ctx['errno'], + $ctx['error'], + 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html' + ); + $uriString = (string) $easy->request->getUri(); + if ($uriString !== '' && false === \strpos($ctx['error'], $uriString)) { + $message .= \sprintf(' for %s', $uriString); + } + + // Create a connection exception if it was a specific error code. + $error = isset($connectionErrors[$easy->errno]) + ? new ConnectException($message, $easy->request, null, $ctx) + : new RequestException($message, $easy->request, $easy->response, null, $ctx); + + return P\Create::rejectionFor($error); + } + + /** + * @return array + */ + private function getDefaultConf(EasyHandle $easy): array + { + $conf = [ + '_headers' => $easy->request->getHeaders(), + \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), + \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), + \CURLOPT_RETURNTRANSFER => false, + \CURLOPT_HEADER => false, + \CURLOPT_CONNECTTIMEOUT => 150, + ]; + + if (\defined('CURLOPT_PROTOCOLS')) { + $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS; + } + + $version = $easy->request->getProtocolVersion(); + if ($version == 1.1) { + $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1; + } elseif ($version == 2.0) { + $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0; + } else { + $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0; + } + + return $conf; + } + + private function applyMethod(EasyHandle $easy, array &$conf): void + { + $body = $easy->request->getBody(); + $size = $body->getSize(); + + if ($size === null || $size > 0) { + $this->applyBody($easy->request, $easy->options, $conf); + return; + } + + $method = $easy->request->getMethod(); + if ($method === 'PUT' || $method === 'POST') { + // See https://tools.ietf.org/html/rfc7230#section-3.3.2 + if (!$easy->request->hasHeader('Content-Length')) { + $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0'; + } + } elseif ($method === 'HEAD') { + $conf[\CURLOPT_NOBODY] = true; + unset( + $conf[\CURLOPT_WRITEFUNCTION], + $conf[\CURLOPT_READFUNCTION], + $conf[\CURLOPT_FILE], + $conf[\CURLOPT_INFILE] + ); + } + } + + private function applyBody(RequestInterface $request, array $options, array &$conf): void + { + $size = $request->hasHeader('Content-Length') + ? (int) $request->getHeaderLine('Content-Length') + : null; + + // Send the body as a string if the size is less than 1MB OR if the + // [curl][body_as_string] request value is set. + if (($size !== null && $size < 1000000) || !empty($options['_body_as_string'])) { + $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody(); + // Don't duplicate the Content-Length header + $this->removeHeader('Content-Length', $conf); + $this->removeHeader('Transfer-Encoding', $conf); + } else { + $conf[\CURLOPT_UPLOAD] = true; + if ($size !== null) { + $conf[\CURLOPT_INFILESIZE] = $size; + $this->removeHeader('Content-Length', $conf); + } + $body = $request->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) { + return $body->read($length); + }; + } + + // If the Expect header is not present, prevent curl from adding it + if (!$request->hasHeader('Expect')) { + $conf[\CURLOPT_HTTPHEADER][] = 'Expect:'; + } + + // cURL sometimes adds a content-type by default. Prevent this. + if (!$request->hasHeader('Content-Type')) { + $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:'; + } + } + + private function applyHeaders(EasyHandle $easy, array &$conf): void + { + foreach ($conf['_headers'] as $name => $values) { + foreach ($values as $value) { + $value = (string) $value; + if ($value === '') { + // cURL requires a special format for empty headers. + // See https://github.com/guzzle/guzzle/issues/1882 for more details. + $conf[\CURLOPT_HTTPHEADER][] = "$name;"; + } else { + $conf[\CURLOPT_HTTPHEADER][] = "$name: $value"; + } + } + } + + // Remove the Accept header if one was not set + if (!$easy->request->hasHeader('Accept')) { + $conf[\CURLOPT_HTTPHEADER][] = 'Accept:'; + } + } + + /** + * Remove a header from the options array. + * + * @param string $name Case-insensitive header to remove + * @param array $options Array of options to modify + */ + private function removeHeader(string $name, array &$options): void + { + foreach (\array_keys($options['_headers']) as $key) { + if (!\strcasecmp($key, $name)) { + unset($options['_headers'][$key]); + return; + } + } + } + + private function applyHandlerOptions(EasyHandle $easy, array &$conf): void + { + $options = $easy->options; + if (isset($options['verify'])) { + if ($options['verify'] === false) { + unset($conf[\CURLOPT_CAINFO]); + $conf[\CURLOPT_SSL_VERIFYHOST] = 0; + $conf[\CURLOPT_SSL_VERIFYPEER] = false; + } else { + $conf[\CURLOPT_SSL_VERIFYHOST] = 2; + $conf[\CURLOPT_SSL_VERIFYPEER] = true; + if (\is_string($options['verify'])) { + // Throw an error if the file/folder/link path is not valid or doesn't exist. + if (!\file_exists($options['verify'])) { + throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}"); + } + // If it's a directory or a link to a directory use CURLOPT_CAPATH. + // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO. + if ( + \is_dir($options['verify']) || + ( + \is_link($options['verify']) === true && + ($verifyLink = \readlink($options['verify'])) !== false && + \is_dir($verifyLink) + ) + ) { + $conf[\CURLOPT_CAPATH] = $options['verify']; + } else { + $conf[\CURLOPT_CAINFO] = $options['verify']; + } + } + } + } + + if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) { + $accept = $easy->request->getHeaderLine('Accept-Encoding'); + if ($accept) { + $conf[\CURLOPT_ENCODING] = $accept; + } else { + $conf[\CURLOPT_ENCODING] = ''; + // Don't let curl send the header over the wire + $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:'; + } + } + + if (!isset($options['sink'])) { + // Use a default temp stream if no sink was set. + $options['sink'] = \GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+'); + } + $sink = $options['sink']; + if (!\is_string($sink)) { + $sink = \GuzzleHttp\Psr7\Utils::streamFor($sink); + } elseif (!\is_dir(\dirname($sink))) { + // Ensure that the directory exists before failing in curl. + throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink)); + } else { + $sink = new LazyOpenStream($sink, 'w+'); + } + $easy->sink = $sink; + $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int { + return $sink->write($write); + }; + + $timeoutRequiresNoSignal = false; + if (isset($options['timeout'])) { + $timeoutRequiresNoSignal |= $options['timeout'] < 1; + $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000; + } + + // CURL default value is CURL_IPRESOLVE_WHATEVER + if (isset($options['force_ip_resolve'])) { + if ('v4' === $options['force_ip_resolve']) { + $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4; + } elseif ('v6' === $options['force_ip_resolve']) { + $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6; + } + } + + if (isset($options['connect_timeout'])) { + $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1; + $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000; + } + + if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') { + $conf[\CURLOPT_NOSIGNAL] = true; + } + + if (isset($options['proxy'])) { + if (!\is_array($options['proxy'])) { + $conf[\CURLOPT_PROXY] = $options['proxy']; + } else { + $scheme = $easy->request->getUri()->getScheme(); + if (isset($options['proxy'][$scheme])) { + $host = $easy->request->getUri()->getHost(); + if (!isset($options['proxy']['no']) || !Utils::isHostInNoProxy($host, $options['proxy']['no'])) { + $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme]; + } + } + } + } + + if (isset($options['cert'])) { + $cert = $options['cert']; + if (\is_array($cert)) { + $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1]; + $cert = $cert[0]; + } + if (!\file_exists($cert)) { + throw new \InvalidArgumentException("SSL certificate not found: {$cert}"); + } + # OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files. + # see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html + $ext = pathinfo($cert, \PATHINFO_EXTENSION); + if (preg_match('#^(der|p12)$#i', $ext)) { + $conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext); + } + $conf[\CURLOPT_SSLCERT] = $cert; + } + + if (isset($options['ssl_key'])) { + if (\is_array($options['ssl_key'])) { + if (\count($options['ssl_key']) === 2) { + [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key']; + } else { + [$sslKey] = $options['ssl_key']; + } + } + + $sslKey = $sslKey ?? $options['ssl_key']; + + if (!\file_exists($sslKey)) { + throw new \InvalidArgumentException("SSL private key not found: {$sslKey}"); + } + $conf[\CURLOPT_SSLKEY] = $sslKey; + } + + if (isset($options['progress'])) { + $progress = $options['progress']; + if (!\is_callable($progress)) { + throw new \InvalidArgumentException('progress client option must be callable'); + } + $conf[\CURLOPT_NOPROGRESS] = false; + $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) { + $progress($downloadSize, $downloaded, $uploadSize, $uploaded); + }; + } + + if (!empty($options['debug'])) { + $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']); + $conf[\CURLOPT_VERBOSE] = true; + } + } + + /** + * This function ensures that a response was set on a transaction. If one + * was not set, then the request is retried if possible. This error + * typically means you are sending a payload, curl encountered a + * "Connection died, retrying a fresh connect" error, tried to rewind the + * stream, and then encountered a "necessary data rewind wasn't possible" + * error, causing the request to be sent through curl_multi_info_read() + * without an error status. + * + * @param callable(RequestInterface, array): PromiseInterface $handler + */ + private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface + { + try { + // Only rewind if the body has been read from. + $body = $easy->request->getBody(); + if ($body->tell() > 0) { + $body->rewind(); + } + } catch (\RuntimeException $e) { + $ctx['error'] = 'The connection unexpectedly failed without ' + . 'providing an error. The request would have been retried, ' + . 'but attempting to rewind the request body failed. ' + . 'Exception: ' . $e; + return self::createRejection($easy, $ctx); + } + + // Retry no more than 3 times before giving up. + if (!isset($easy->options['_curl_retries'])) { + $easy->options['_curl_retries'] = 1; + } elseif ($easy->options['_curl_retries'] == 2) { + $ctx['error'] = 'The cURL request was retried 3 times ' + . 'and did not succeed. The most likely reason for the failure ' + . 'is that cURL was unable to rewind the body of the request ' + . 'and subsequent retries resulted in the same error. Turn on ' + . 'the debug option to see what went wrong. See ' + . 'https://bugs.php.net/bug.php?id=47204 for more information.'; + return self::createRejection($easy, $ctx); + } else { + $easy->options['_curl_retries']++; + } + + return $handler($easy->request, $easy->options); + } + + private function createHeaderFn(EasyHandle $easy): callable + { + if (isset($easy->options['on_headers'])) { + $onHeaders = $easy->options['on_headers']; + + if (!\is_callable($onHeaders)) { + throw new \InvalidArgumentException('on_headers must be callable'); + } + } else { + $onHeaders = null; + } + + return static function ($ch, $h) use ( + $onHeaders, + $easy, + &$startingResponse + ) { + $value = \trim($h); + if ($value === '') { + $startingResponse = true; + try { + $easy->createResponse(); + } catch (\Exception $e) { + $easy->createResponseException = $e; + return -1; + } + if ($onHeaders !== null) { + try { + $onHeaders($easy->response); + } catch (\Exception $e) { + // Associate the exception with the handle and trigger + // a curl header write error by returning 0. + $easy->onHeadersException = $e; + return -1; + } + } + } elseif ($startingResponse) { + $startingResponse = false; + $easy->headers = [$value]; + } else { + $easy->headers[] = $value; + } + return \strlen($h); + }; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php new file mode 100644 index 0000000..fe57ed5 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php @@ -0,0 +1,25 @@ +factory = $options['handle_factory'] + ?? new CurlFactory(3); + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + if (isset($options['delay'])) { + \usleep($options['delay'] * 1000); + } + + $easy = $this->factory->create($request, $options); + \curl_exec($easy->handle); + $easy->errno = \curl_errno($easy->handle); + + return CurlFactory::finish($this, $easy, $this->factory); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php new file mode 100644 index 0000000..4e31263 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php @@ -0,0 +1,253 @@ + An array of delay times, indexed by handle id in `addRequest`. + * + * @see CurlMultiHandler::addRequest + */ + private $delays = []; + + /** + * @var array An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt() + */ + private $options = []; + + /** + * This handler accepts the following options: + * + * - handle_factory: An optional factory used to create curl handles + * - select_timeout: Optional timeout (in seconds) to block before timing + * out while selecting curl handles. Defaults to 1 second. + * - options: An associative array of CURLMOPT_* options and + * corresponding values for curl_multi_setopt() + */ + public function __construct(array $options = []) + { + $this->factory = $options['handle_factory'] ?? new CurlFactory(50); + + if (isset($options['select_timeout'])) { + $this->selectTimeout = $options['select_timeout']; + } elseif ($selectTimeout = Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) { + @trigger_error('Since guzzlehttp/guzzle 7.2.0: Using environment variable GUZZLE_CURL_SELECT_TIMEOUT is deprecated. Use option "select_timeout" instead.', \E_USER_DEPRECATED); + $this->selectTimeout = (int) $selectTimeout; + } else { + $this->selectTimeout = 1; + } + + $this->options = $options['options'] ?? []; + } + + /** + * @param string $name + * + * @return resource|\CurlMultiHandle + * + * @throws \BadMethodCallException when another field as `_mh` will be gotten + * @throws \RuntimeException when curl can not initialize a multi handle + */ + public function __get($name) + { + if ($name !== '_mh') { + throw new \BadMethodCallException("Can not get other property as '_mh'."); + } + + $multiHandle = \curl_multi_init(); + + if (false === $multiHandle) { + throw new \RuntimeException('Can not initialize curl multi handle.'); + } + + $this->_mh = $multiHandle; + + foreach ($this->options as $option => $value) { + // A warning is raised in case of a wrong option. + curl_multi_setopt($this->_mh, $option, $value); + } + + return $this->_mh; + } + + public function __destruct() + { + if (isset($this->_mh)) { + \curl_multi_close($this->_mh); + unset($this->_mh); + } + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + $easy = $this->factory->create($request, $options); + $id = (int) $easy->handle; + + $promise = new Promise( + [$this, 'execute'], + function () use ($id) { + return $this->cancel($id); + } + ); + + $this->addRequest(['easy' => $easy, 'deferred' => $promise]); + + return $promise; + } + + /** + * Ticks the curl event loop. + */ + public function tick(): void + { + // Add any delayed handles if needed. + if ($this->delays) { + $currentTime = Utils::currentTime(); + foreach ($this->delays as $id => $delay) { + if ($currentTime >= $delay) { + unset($this->delays[$id]); + \curl_multi_add_handle( + $this->_mh, + $this->handles[$id]['easy']->handle + ); + } + } + } + + // Step through the task queue which may add additional requests. + P\Utils::queue()->run(); + + if ($this->active && \curl_multi_select($this->_mh, $this->selectTimeout) === -1) { + // Perform a usleep if a select returns -1. + // See: https://bugs.php.net/bug.php?id=61141 + \usleep(250); + } + + while (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM); + + $this->processMessages(); + } + + /** + * Runs until all outstanding connections have completed. + */ + public function execute(): void + { + $queue = P\Utils::queue(); + + while ($this->handles || !$queue->isEmpty()) { + // If there are no transfers, then sleep for the next delay + if (!$this->active && $this->delays) { + \usleep($this->timeToNext()); + } + $this->tick(); + } + } + + private function addRequest(array $entry): void + { + $easy = $entry['easy']; + $id = (int) $easy->handle; + $this->handles[$id] = $entry; + if (empty($easy->options['delay'])) { + \curl_multi_add_handle($this->_mh, $easy->handle); + } else { + $this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000); + } + } + + /** + * Cancels a handle from sending and removes references to it. + * + * @param int $id Handle ID to cancel and remove. + * + * @return bool True on success, false on failure. + */ + private function cancel($id): bool + { + // Cannot cancel if it has been processed. + if (!isset($this->handles[$id])) { + return false; + } + + $handle = $this->handles[$id]['easy']->handle; + unset($this->delays[$id], $this->handles[$id]); + \curl_multi_remove_handle($this->_mh, $handle); + \curl_close($handle); + + return true; + } + + private function processMessages(): void + { + while ($done = \curl_multi_info_read($this->_mh)) { + $id = (int) $done['handle']; + \curl_multi_remove_handle($this->_mh, $done['handle']); + + if (!isset($this->handles[$id])) { + // Probably was cancelled. + continue; + } + + $entry = $this->handles[$id]; + unset($this->handles[$id], $this->delays[$id]); + $entry['easy']->errno = $done['result']; + $entry['deferred']->resolve( + CurlFactory::finish($this, $entry['easy'], $this->factory) + ); + } + } + + private function timeToNext(): int + { + $currentTime = Utils::currentTime(); + $nextTime = \PHP_INT_MAX; + foreach ($this->delays as $time) { + if ($time < $nextTime) { + $nextTime = $time; + } + } + + return ((int) \max(0, $nextTime - $currentTime)) * 1000000; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php new file mode 100644 index 0000000..224344d --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php @@ -0,0 +1,112 @@ +headers); + + $normalizedKeys = Utils::normalizeHeaderKeys($headers); + + if (!empty($this->options['decode_content']) && isset($normalizedKeys['content-encoding'])) { + $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; + unset($headers[$normalizedKeys['content-encoding']]); + if (isset($normalizedKeys['content-length'])) { + $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; + + $bodyLength = (int) $this->sink->getSize(); + if ($bodyLength) { + $headers[$normalizedKeys['content-length']] = $bodyLength; + } else { + unset($headers[$normalizedKeys['content-length']]); + } + } + } + + // Attach a response to the easy handle with the parsed headers. + $this->response = new Response( + $status, + $headers, + $this->sink, + $ver, + $reason + ); + } + + /** + * @param string $name + * + * @return void + * + * @throws \BadMethodCallException + */ + public function __get($name) + { + $msg = $name === 'handle' ? 'The EasyHandle has been released' : 'Invalid property: ' . $name; + throw new \BadMethodCallException($msg); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php new file mode 100644 index 0000000..a098884 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php @@ -0,0 +1,42 @@ +|null $queue The parameters to be passed to the append function, as an indexed array. + * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. + * @param callable|null $onRejected Callback to invoke when the return value is rejected. + */ + public function __construct(array $queue = null, callable $onFulfilled = null, callable $onRejected = null) + { + $this->onFulfilled = $onFulfilled; + $this->onRejected = $onRejected; + + if ($queue) { + // array_values included for BC + $this->append(...array_values($queue)); + } + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + if (!$this->queue) { + throw new \OutOfBoundsException('Mock queue is empty'); + } + + if (isset($options['delay']) && \is_numeric($options['delay'])) { + \usleep((int) $options['delay'] * 1000); + } + + $this->lastRequest = $request; + $this->lastOptions = $options; + $response = \array_shift($this->queue); + + if (isset($options['on_headers'])) { + if (!\is_callable($options['on_headers'])) { + throw new \InvalidArgumentException('on_headers must be callable'); + } + try { + $options['on_headers']($response); + } catch (\Exception $e) { + $msg = 'An error was encountered during the on_headers event'; + $response = new RequestException($msg, $request, $response, $e); + } + } + + if (\is_callable($response)) { + $response = $response($request, $options); + } + + $response = $response instanceof \Throwable + ? P\Create::rejectionFor($response) + : P\Create::promiseFor($response); + + return $response->then( + function (?ResponseInterface $value) use ($request, $options) { + $this->invokeStats($request, $options, $value); + if ($this->onFulfilled) { + ($this->onFulfilled)($value); + } + + if ($value !== null && isset($options['sink'])) { + $contents = (string) $value->getBody(); + $sink = $options['sink']; + + if (\is_resource($sink)) { + \fwrite($sink, $contents); + } elseif (\is_string($sink)) { + \file_put_contents($sink, $contents); + } elseif ($sink instanceof StreamInterface) { + $sink->write($contents); + } + } + + return $value; + }, + function ($reason) use ($request, $options) { + $this->invokeStats($request, $options, null, $reason); + if ($this->onRejected) { + ($this->onRejected)($reason); + } + return P\Create::rejectionFor($reason); + } + ); + } + + /** + * Adds one or more variadic requests, exceptions, callables, or promises + * to the queue. + * + * @param mixed ...$values + */ + public function append(...$values): void + { + foreach ($values as $value) { + if ($value instanceof ResponseInterface + || $value instanceof \Throwable + || $value instanceof PromiseInterface + || \is_callable($value) + ) { + $this->queue[] = $value; + } else { + throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found ' . Utils::describeType($value)); + } + } + } + + /** + * Get the last received request. + */ + public function getLastRequest(): ?RequestInterface + { + return $this->lastRequest; + } + + /** + * Get the last received request options. + */ + public function getLastOptions(): array + { + return $this->lastOptions; + } + + /** + * Returns the number of remaining items in the queue. + */ + public function count(): int + { + return \count($this->queue); + } + + public function reset(): void + { + $this->queue = []; + } + + /** + * @param mixed $reason Promise or reason. + */ + private function invokeStats( + RequestInterface $request, + array $options, + ResponseInterface $response = null, + $reason = null + ): void { + if (isset($options['on_stats'])) { + $transferTime = $options['transfer_time'] ?? 0; + $stats = new TransferStats($request, $response, $transferTime, $reason); + ($options['on_stats'])($stats); + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php new file mode 100644 index 0000000..f045b52 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php @@ -0,0 +1,51 @@ +withoutHeader('Expect'); + + // Append a content-length header if body size is zero to match + // cURL's behavior. + if (0 === $request->getBody()->getSize()) { + $request = $request->withHeader('Content-Length', '0'); + } + + return $this->createResponse( + $request, + $options, + $this->createStream($request, $options), + $startTime + ); + } catch (\InvalidArgumentException $e) { + throw $e; + } catch (\Exception $e) { + // Determine if the error was a networking error. + $message = $e->getMessage(); + // This list can probably get more comprehensive. + if (false !== \strpos($message, 'getaddrinfo') // DNS lookup failed + || false !== \strpos($message, 'Connection refused') + || false !== \strpos($message, "couldn't connect to host") // error on HHVM + || false !== \strpos($message, "connection attempt failed") + ) { + $e = new ConnectException($e->getMessage(), $request, $e); + } else { + $e = RequestException::wrapException($request, $e); + } + $this->invokeStats($options, $request, $startTime, null, $e); + + return P\Create::rejectionFor($e); + } + } + + private function invokeStats( + array $options, + RequestInterface $request, + ?float $startTime, + ResponseInterface $response = null, + \Throwable $error = null + ): void { + if (isset($options['on_stats'])) { + $stats = new TransferStats($request, $response, Utils::currentTime() - $startTime, $error, []); + ($options['on_stats'])($stats); + } + } + + /** + * @param resource $stream + */ + private function createResponse(RequestInterface $request, array $options, $stream, ?float $startTime): PromiseInterface + { + $hdrs = $this->lastHeaders; + $this->lastHeaders = []; + + try { + [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs); + } catch (\Exception $e) { + return P\Create::rejectionFor( + new RequestException('An error was encountered while creating the response', $request, null, $e) + ); + } + + [$stream, $headers] = $this->checkDecode($options, $headers, $stream); + $stream = Psr7\Utils::streamFor($stream); + $sink = $stream; + + if (\strcasecmp('HEAD', $request->getMethod())) { + $sink = $this->createSink($stream, $options); + } + + try { + $response = new Psr7\Response($status, $headers, $sink, $ver, $reason); + } catch (\Exception $e) { + return P\Create::rejectionFor( + new RequestException('An error was encountered while creating the response', $request, null, $e) + ); + } + + if (isset($options['on_headers'])) { + try { + $options['on_headers']($response); + } catch (\Exception $e) { + return P\Create::rejectionFor( + new RequestException('An error was encountered during the on_headers event', $request, $response, $e) + ); + } + } + + // Do not drain when the request is a HEAD request because they have + // no body. + if ($sink !== $stream) { + $this->drain($stream, $sink, $response->getHeaderLine('Content-Length')); + } + + $this->invokeStats($options, $request, $startTime, $response, null); + + return new FulfilledPromise($response); + } + + private function createSink(StreamInterface $stream, array $options): StreamInterface + { + if (!empty($options['stream'])) { + return $stream; + } + + $sink = $options['sink'] ?? Psr7\Utils::tryFopen('php://temp', 'r+'); + + return \is_string($sink) ? new Psr7\LazyOpenStream($sink, 'w+') : Psr7\Utils::streamFor($sink); + } + + /** + * @param resource $stream + */ + private function checkDecode(array $options, array $headers, $stream): array + { + // Automatically decode responses when instructed. + if (!empty($options['decode_content'])) { + $normalizedKeys = Utils::normalizeHeaderKeys($headers); + if (isset($normalizedKeys['content-encoding'])) { + $encoding = $headers[$normalizedKeys['content-encoding']]; + if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') { + $stream = new Psr7\InflateStream(Psr7\Utils::streamFor($stream)); + $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; + + // Remove content-encoding header + unset($headers[$normalizedKeys['content-encoding']]); + + // Fix content-length header + if (isset($normalizedKeys['content-length'])) { + $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; + $length = (int) $stream->getSize(); + if ($length === 0) { + unset($headers[$normalizedKeys['content-length']]); + } else { + $headers[$normalizedKeys['content-length']] = [$length]; + } + } + } + } + } + + return [$stream, $headers]; + } + + /** + * Drains the source stream into the "sink" client option. + * + * @param string $contentLength Header specifying the amount of + * data to read. + * + * @throws \RuntimeException when the sink option is invalid. + */ + private function drain(StreamInterface $source, StreamInterface $sink, string $contentLength): StreamInterface + { + // If a content-length header is provided, then stop reading once + // that number of bytes has been read. This can prevent infinitely + // reading from a stream when dealing with servers that do not honor + // Connection: Close headers. + Psr7\Utils::copyToStream( + $source, + $sink, + (\strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1 + ); + + $sink->seek(0); + $source->close(); + + return $sink; + } + + /** + * Create a resource and check to ensure it was created successfully + * + * @param callable $callback Callable that returns stream resource + * + * @return resource + * + * @throws \RuntimeException on error + */ + private function createResource(callable $callback) + { + $errors = []; + \set_error_handler(static function ($_, $msg, $file, $line) use (&$errors): bool { + $errors[] = [ + 'message' => $msg, + 'file' => $file, + 'line' => $line + ]; + return true; + }); + + $resource = $callback(); + \restore_error_handler(); + + if (!$resource) { + $message = 'Error creating resource: '; + foreach ($errors as $err) { + foreach ($err as $key => $value) { + $message .= "[$key] $value" . \PHP_EOL; + } + } + throw new \RuntimeException(\trim($message)); + } + + return $resource; + } + + /** + * @return resource + */ + private function createStream(RequestInterface $request, array $options) + { + static $methods; + if (!$methods) { + $methods = \array_flip(\get_class_methods(__CLASS__)); + } + + // HTTP/1.1 streams using the PHP stream wrapper require a + // Connection: close header + if ($request->getProtocolVersion() == '1.1' + && !$request->hasHeader('Connection') + ) { + $request = $request->withHeader('Connection', 'close'); + } + + // Ensure SSL is verified by default + if (!isset($options['verify'])) { + $options['verify'] = true; + } + + $params = []; + $context = $this->getDefaultContext($request); + + if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) { + throw new \InvalidArgumentException('on_headers must be callable'); + } + + if (!empty($options)) { + foreach ($options as $key => $value) { + $method = "add_{$key}"; + if (isset($methods[$method])) { + $this->{$method}($request, $context, $value, $params); + } + } + } + + if (isset($options['stream_context'])) { + if (!\is_array($options['stream_context'])) { + throw new \InvalidArgumentException('stream_context must be an array'); + } + $context = \array_replace_recursive($context, $options['stream_context']); + } + + // Microsoft NTLM authentication only supported with curl handler + if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) { + throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler'); + } + + $uri = $this->resolveHost($request, $options); + + $contextResource = $this->createResource( + static function () use ($context, $params) { + return \stream_context_create($context, $params); + } + ); + + return $this->createResource( + function () use ($uri, &$http_response_header, $contextResource, $context, $options, $request) { + $resource = @\fopen((string) $uri, 'r', false, $contextResource); + $this->lastHeaders = $http_response_header; + + if (false === $resource) { + throw new ConnectException(sprintf('Connection refused for URI %s', $uri), $request, null, $context); + } + + if (isset($options['read_timeout'])) { + $readTimeout = $options['read_timeout']; + $sec = (int) $readTimeout; + $usec = ($readTimeout - $sec) * 100000; + \stream_set_timeout($resource, $sec, $usec); + } + + return $resource; + } + ); + } + + private function resolveHost(RequestInterface $request, array $options): UriInterface + { + $uri = $request->getUri(); + + if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) { + if ('v4' === $options['force_ip_resolve']) { + $records = \dns_get_record($uri->getHost(), \DNS_A); + if (false === $records || !isset($records[0]['ip'])) { + throw new ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); + } + return $uri->withHost($records[0]['ip']); + } + if ('v6' === $options['force_ip_resolve']) { + $records = \dns_get_record($uri->getHost(), \DNS_AAAA); + if (false === $records || !isset($records[0]['ipv6'])) { + throw new ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); + } + return $uri->withHost('[' . $records[0]['ipv6'] . ']'); + } + } + + return $uri; + } + + private function getDefaultContext(RequestInterface $request): array + { + $headers = ''; + foreach ($request->getHeaders() as $name => $value) { + foreach ($value as $val) { + $headers .= "$name: $val\r\n"; + } + } + + $context = [ + 'http' => [ + 'method' => $request->getMethod(), + 'header' => $headers, + 'protocol_version' => $request->getProtocolVersion(), + 'ignore_errors' => true, + 'follow_location' => 0, + ], + ]; + + $body = (string) $request->getBody(); + + if (!empty($body)) { + $context['http']['content'] = $body; + // Prevent the HTTP handler from adding a Content-Type header. + if (!$request->hasHeader('Content-Type')) { + $context['http']['header'] .= "Content-Type:\r\n"; + } + } + + $context['http']['header'] = \rtrim($context['http']['header']); + + return $context; + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): void + { + $uri = null; + + if (!\is_array($value)) { + $uri = $value; + } else { + $scheme = $request->getUri()->getScheme(); + if (isset($value[$scheme])) { + if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) { + $uri = $value[$scheme]; + } + } + } + + if (!$uri) { + return; + } + + $parsed = $this->parse_proxy($uri); + $options['http']['proxy'] = $parsed['proxy']; + + if ($parsed['auth']) { + if (!isset($options['http']['header'])) { + $options['http']['header'] = []; + } + $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}"; + } + } + + /** + * Parses the given proxy URL to make it compatible with the format PHP's stream context expects. + */ + private function parse_proxy(string $url): array + { + $parsed = \parse_url($url); + + if ($parsed !== false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') { + if (isset($parsed['host']) && isset($parsed['port'])) { + $auth = null; + if (isset($parsed['user']) && isset($parsed['pass'])) { + $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}"); + } + + return [ + 'proxy' => "tcp://{$parsed['host']}:{$parsed['port']}", + 'auth' => $auth ? "Basic {$auth}" : null, + ]; + } + } + + // Return proxy as-is. + return [ + 'proxy' => $url, + 'auth' => null, + ]; + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_timeout(RequestInterface $request, array &$options, $value, array &$params): void + { + if ($value > 0) { + $options['http']['timeout'] = $value; + } + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_verify(RequestInterface $request, array &$options, $value, array &$params): void + { + if ($value === false) { + $options['ssl']['verify_peer'] = false; + $options['ssl']['verify_peer_name'] = false; + + return; + } + + if (\is_string($value)) { + $options['ssl']['cafile'] = $value; + if (!\file_exists($value)) { + throw new \RuntimeException("SSL CA bundle not found: $value"); + } + } elseif ($value !== true) { + throw new \InvalidArgumentException('Invalid verify request option'); + } + + $options['ssl']['verify_peer'] = true; + $options['ssl']['verify_peer_name'] = true; + $options['ssl']['allow_self_signed'] = false; + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void + { + if (\is_array($value)) { + $options['ssl']['passphrase'] = $value[1]; + $value = $value[0]; + } + + if (!\file_exists($value)) { + throw new \RuntimeException("SSL certificate not found: {$value}"); + } + + $options['ssl']['local_cert'] = $value; + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void + { + self::addNotification( + $params, + static function ($code, $a, $b, $c, $transferred, $total) use ($value) { + if ($code == \STREAM_NOTIFY_PROGRESS) { + $value($total, $transferred, null, null); + } + } + ); + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_debug(RequestInterface $request, array &$options, $value, array &$params): void + { + if ($value === false) { + return; + } + + static $map = [ + \STREAM_NOTIFY_CONNECT => 'CONNECT', + \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', + \STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', + \STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', + \STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', + \STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', + \STREAM_NOTIFY_PROGRESS => 'PROGRESS', + \STREAM_NOTIFY_FAILURE => 'FAILURE', + \STREAM_NOTIFY_COMPLETED => 'COMPLETED', + \STREAM_NOTIFY_RESOLVE => 'RESOLVE', + ]; + static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max']; + + $value = Utils::debugResource($value); + $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment(''); + self::addNotification( + $params, + static function (int $code, ...$passed) use ($ident, $value, $map, $args): void { + \fprintf($value, '<%s> [%s] ', $ident, $map[$code]); + foreach (\array_filter($passed) as $i => $v) { + \fwrite($value, $args[$i] . ': "' . $v . '" '); + } + \fwrite($value, "\n"); + } + ); + } + + private static function addNotification(array &$params, callable $notify): void + { + // Wrap the existing function if needed. + if (!isset($params['notification'])) { + $params['notification'] = $notify; + } else { + $params['notification'] = self::callArray([ + $params['notification'], + $notify + ]); + } + } + + private static function callArray(array $functions): callable + { + return static function (...$args) use ($functions) { + foreach ($functions as $fn) { + $fn(...$args); + } + }; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/HandlerStack.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/HandlerStack.php new file mode 100644 index 0000000..b67239e --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/HandlerStack.php @@ -0,0 +1,271 @@ +push(Middleware::httpErrors(), 'http_errors'); + $stack->push(Middleware::redirect(), 'allow_redirects'); + $stack->push(Middleware::cookies(), 'cookies'); + $stack->push(Middleware::prepareBody(), 'prepare_body'); + + return $stack; + } + + /** + * @param null|callable(RequestInterface, array): PromiseInterface $handler Underlying HTTP handler. + */ + public function __construct(callable $handler = null) + { + $this->handler = $handler; + } + + /** + * Invokes the handler stack as a composed handler + * + * @return ResponseInterface|PromiseInterface + */ + public function __invoke(RequestInterface $request, array $options) + { + $handler = $this->resolve(); + + return $handler($request, $options); + } + + /** + * Dumps a string representation of the stack. + * + * @return string + */ + public function __toString() + { + $depth = 0; + $stack = []; + + if ($this->handler !== null) { + $stack[] = "0) Handler: " . $this->debugCallable($this->handler); + } + + $result = ''; + foreach (\array_reverse($this->stack) as $tuple) { + $depth++; + $str = "{$depth}) Name: '{$tuple[1]}', "; + $str .= "Function: " . $this->debugCallable($tuple[0]); + $result = "> {$str}\n{$result}"; + $stack[] = $str; + } + + foreach (\array_keys($stack) as $k) { + $result .= "< {$stack[$k]}\n"; + } + + return $result; + } + + /** + * Set the HTTP handler that actually returns a promise. + * + * @param callable(RequestInterface, array): PromiseInterface $handler Accepts a request and array of options and + * returns a Promise. + */ + public function setHandler(callable $handler): void + { + $this->handler = $handler; + $this->cached = null; + } + + /** + * Returns true if the builder has a handler. + */ + public function hasHandler(): bool + { + return $this->handler !== null ; + } + + /** + * Unshift a middleware to the bottom of the stack. + * + * @param callable(callable): callable $middleware Middleware function + * @param string $name Name to register for this middleware. + */ + public function unshift(callable $middleware, ?string $name = null): void + { + \array_unshift($this->stack, [$middleware, $name]); + $this->cached = null; + } + + /** + * Push a middleware to the top of the stack. + * + * @param callable(callable): callable $middleware Middleware function + * @param string $name Name to register for this middleware. + */ + public function push(callable $middleware, string $name = ''): void + { + $this->stack[] = [$middleware, $name]; + $this->cached = null; + } + + /** + * Add a middleware before another middleware by name. + * + * @param string $findName Middleware to find + * @param callable(callable): callable $middleware Middleware function + * @param string $withName Name to register for this middleware. + */ + public function before(string $findName, callable $middleware, string $withName = ''): void + { + $this->splice($findName, $withName, $middleware, true); + } + + /** + * Add a middleware after another middleware by name. + * + * @param string $findName Middleware to find + * @param callable(callable): callable $middleware Middleware function + * @param string $withName Name to register for this middleware. + */ + public function after(string $findName, callable $middleware, string $withName = ''): void + { + $this->splice($findName, $withName, $middleware, false); + } + + /** + * Remove a middleware by instance or name from the stack. + * + * @param callable|string $remove Middleware to remove by instance or name. + */ + public function remove($remove): void + { + $this->cached = null; + $idx = \is_callable($remove) ? 0 : 1; + $this->stack = \array_values(\array_filter( + $this->stack, + static function ($tuple) use ($idx, $remove) { + return $tuple[$idx] !== $remove; + } + )); + } + + /** + * Compose the middleware and handler into a single callable function. + * + * @return callable(RequestInterface, array): PromiseInterface + */ + public function resolve(): callable + { + if ($this->cached === null) { + if (($prev = $this->handler) === null) { + throw new \LogicException('No handler has been specified'); + } + + foreach (\array_reverse($this->stack) as $fn) { + /** @var callable(RequestInterface, array): PromiseInterface $prev */ + $prev = $fn[0]($prev); + } + + $this->cached = $prev; + } + + return $this->cached; + } + + private function findByName(string $name): int + { + foreach ($this->stack as $k => $v) { + if ($v[1] === $name) { + return $k; + } + } + + throw new \InvalidArgumentException("Middleware not found: $name"); + } + + /** + * Splices a function into the middleware list at a specific position. + */ + private function splice(string $findName, string $withName, callable $middleware, bool $before): void + { + $this->cached = null; + $idx = $this->findByName($findName); + $tuple = [$middleware, $withName]; + + if ($before) { + if ($idx === 0) { + \array_unshift($this->stack, $tuple); + } else { + $replacement = [$tuple, $this->stack[$idx]]; + \array_splice($this->stack, $idx, 1, $replacement); + } + } elseif ($idx === \count($this->stack) - 1) { + $this->stack[] = $tuple; + } else { + $replacement = [$this->stack[$idx], $tuple]; + \array_splice($this->stack, $idx, 1, $replacement); + } + } + + /** + * Provides a debug string for a given callable. + * + * @param callable $fn Function to write as a string. + */ + private function debugCallable($fn): string + { + if (\is_string($fn)) { + return "callable({$fn})"; + } + + if (\is_array($fn)) { + return \is_string($fn[0]) + ? "callable({$fn[0]}::{$fn[1]})" + : "callable(['" . \get_class($fn[0]) . "', '{$fn[1]}'])"; + } + + /** @var object $fn */ + return 'callable(' . \spl_object_hash($fn) . ')'; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/MessageFormatter.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/MessageFormatter.php new file mode 100644 index 0000000..238770f --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/MessageFormatter.php @@ -0,0 +1,198 @@ +>>>>>>>\n{request}\n<<<<<<<<\n{response}\n--------\n{error}"; + public const SHORT = '[{ts}] "{method} {target} HTTP/{version}" {code}'; + + /** + * @var string Template used to format log messages + */ + private $template; + + /** + * @param string $template Log message template + */ + public function __construct(?string $template = self::CLF) + { + $this->template = $template ?: self::CLF; + } + + /** + * Returns a formatted message string. + * + * @param RequestInterface $request Request that was sent + * @param ResponseInterface|null $response Response that was received + * @param \Throwable|null $error Exception that was received + */ + public function format(RequestInterface $request, ?ResponseInterface $response = null, ?\Throwable $error = null): string + { + $cache = []; + + /** @var string */ + return \preg_replace_callback( + '/{\s*([A-Za-z_\-\.0-9]+)\s*}/', + function (array $matches) use ($request, $response, $error, &$cache) { + if (isset($cache[$matches[1]])) { + return $cache[$matches[1]]; + } + + $result = ''; + switch ($matches[1]) { + case 'request': + $result = Psr7\Message::toString($request); + break; + case 'response': + $result = $response ? Psr7\Message::toString($response) : ''; + break; + case 'req_headers': + $result = \trim($request->getMethod() + . ' ' . $request->getRequestTarget()) + . ' HTTP/' . $request->getProtocolVersion() . "\r\n" + . $this->headers($request); + break; + case 'res_headers': + $result = $response ? + \sprintf( + 'HTTP/%s %d %s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ) . "\r\n" . $this->headers($response) + : 'NULL'; + break; + case 'req_body': + $result = $request->getBody()->__toString(); + break; + case 'res_body': + if (!$response instanceof ResponseInterface) { + $result = 'NULL'; + break; + } + + $body = $response->getBody(); + + if (!$body->isSeekable()) { + $result = 'RESPONSE_NOT_LOGGEABLE'; + break; + } + + $result = $response->getBody()->__toString(); + break; + case 'ts': + case 'date_iso_8601': + $result = \gmdate('c'); + break; + case 'date_common_log': + $result = \date('d/M/Y:H:i:s O'); + break; + case 'method': + $result = $request->getMethod(); + break; + case 'version': + $result = $request->getProtocolVersion(); + break; + case 'uri': + case 'url': + $result = $request->getUri(); + break; + case 'target': + $result = $request->getRequestTarget(); + break; + case 'req_version': + $result = $request->getProtocolVersion(); + break; + case 'res_version': + $result = $response + ? $response->getProtocolVersion() + : 'NULL'; + break; + case 'host': + $result = $request->getHeaderLine('Host'); + break; + case 'hostname': + $result = \gethostname(); + break; + case 'code': + $result = $response ? $response->getStatusCode() : 'NULL'; + break; + case 'phrase': + $result = $response ? $response->getReasonPhrase() : 'NULL'; + break; + case 'error': + $result = $error ? $error->getMessage() : 'NULL'; + break; + default: + // handle prefixed dynamic headers + if (\strpos($matches[1], 'req_header_') === 0) { + $result = $request->getHeaderLine(\substr($matches[1], 11)); + } elseif (\strpos($matches[1], 'res_header_') === 0) { + $result = $response + ? $response->getHeaderLine(\substr($matches[1], 11)) + : 'NULL'; + } + } + + $cache[$matches[1]] = $result; + return $result; + }, + $this->template + ); + } + + /** + * Get headers from message as string + */ + private function headers(MessageInterface $message): string + { + $result = ''; + foreach ($message->getHeaders() as $name => $values) { + $result .= $name . ': ' . \implode(', ', $values) . "\r\n"; + } + + return \trim($result); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/MessageFormatterInterface.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/MessageFormatterInterface.php new file mode 100644 index 0000000..a39ac24 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/MessageFormatterInterface.php @@ -0,0 +1,18 @@ +withCookieHeader($request); + return $handler($request, $options) + ->then( + static function (ResponseInterface $response) use ($cookieJar, $request): ResponseInterface { + $cookieJar->extractCookies($request, $response); + return $response; + } + ); + }; + }; + } + + /** + * Middleware that throws exceptions for 4xx or 5xx responses when the + * "http_errors" request option is set to true. + * + * @param BodySummarizerInterface|null $bodySummarizer The body summarizer to use in exception messages. + * + * @return callable(callable): callable Returns a function that accepts the next handler. + */ + public static function httpErrors(BodySummarizerInterface $bodySummarizer = null): callable + { + return static function (callable $handler) use ($bodySummarizer): callable { + return static function ($request, array $options) use ($handler, $bodySummarizer) { + if (empty($options['http_errors'])) { + return $handler($request, $options); + } + return $handler($request, $options)->then( + static function (ResponseInterface $response) use ($request, $bodySummarizer) { + $code = $response->getStatusCode(); + if ($code < 400) { + return $response; + } + throw RequestException::create($request, $response, null, [], $bodySummarizer); + } + ); + }; + }; + } + + /** + * Middleware that pushes history data to an ArrayAccess container. + * + * @param array|\ArrayAccess $container Container to hold the history (by reference). + * + * @return callable(callable): callable Returns a function that accepts the next handler. + * + * @throws \InvalidArgumentException if container is not an array or ArrayAccess. + */ + public static function history(&$container): callable + { + if (!\is_array($container) && !$container instanceof \ArrayAccess) { + throw new \InvalidArgumentException('history container must be an array or object implementing ArrayAccess'); + } + + return static function (callable $handler) use (&$container): callable { + return static function (RequestInterface $request, array $options) use ($handler, &$container) { + return $handler($request, $options)->then( + static function ($value) use ($request, &$container, $options) { + $container[] = [ + 'request' => $request, + 'response' => $value, + 'error' => null, + 'options' => $options + ]; + return $value; + }, + static function ($reason) use ($request, &$container, $options) { + $container[] = [ + 'request' => $request, + 'response' => null, + 'error' => $reason, + 'options' => $options + ]; + return P\Create::rejectionFor($reason); + } + ); + }; + }; + } + + /** + * Middleware that invokes a callback before and after sending a request. + * + * The provided listener cannot modify or alter the response. It simply + * "taps" into the chain to be notified before returning the promise. The + * before listener accepts a request and options array, and the after + * listener accepts a request, options array, and response promise. + * + * @param callable $before Function to invoke before forwarding the request. + * @param callable $after Function invoked after forwarding. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function tap(callable $before = null, callable $after = null): callable + { + return static function (callable $handler) use ($before, $after): callable { + return static function (RequestInterface $request, array $options) use ($handler, $before, $after) { + if ($before) { + $before($request, $options); + } + $response = $handler($request, $options); + if ($after) { + $after($request, $options, $response); + } + return $response; + }; + }; + } + + /** + * Middleware that handles request redirects. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function redirect(): callable + { + return static function (callable $handler): RedirectMiddleware { + return new RedirectMiddleware($handler); + }; + } + + /** + * Middleware that retries requests based on the boolean result of + * invoking the provided "decider" function. + * + * If no delay function is provided, a simple implementation of exponential + * backoff will be utilized. + * + * @param callable $decider Function that accepts the number of retries, + * a request, [response], and [exception] and + * returns true if the request is to be retried. + * @param callable $delay Function that accepts the number of retries and + * returns the number of milliseconds to delay. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function retry(callable $decider, callable $delay = null): callable + { + return static function (callable $handler) use ($decider, $delay): RetryMiddleware { + return new RetryMiddleware($decider, $handler, $delay); + }; + } + + /** + * Middleware that logs requests, responses, and errors using a message + * formatter. + * + * @phpstan-param \Psr\Log\LogLevel::* $logLevel Level at which to log requests. + * + * @param LoggerInterface $logger Logs messages. + * @param MessageFormatterInterface|MessageFormatter $formatter Formatter used to create message strings. + * @param string $logLevel Level at which to log requests. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function log(LoggerInterface $logger, $formatter, string $logLevel = 'info'): callable + { + // To be compatible with Guzzle 7.1.x we need to allow users to pass a MessageFormatter + if (!$formatter instanceof MessageFormatter && !$formatter instanceof MessageFormatterInterface) { + throw new \LogicException(sprintf('Argument 2 to %s::log() must be of type %s', self::class, MessageFormatterInterface::class)); + } + + return static function (callable $handler) use ($logger, $formatter, $logLevel): callable { + return static function (RequestInterface $request, array $options = []) use ($handler, $logger, $formatter, $logLevel) { + return $handler($request, $options)->then( + static function ($response) use ($logger, $request, $formatter, $logLevel): ResponseInterface { + $message = $formatter->format($request, $response); + $logger->log($logLevel, $message); + return $response; + }, + static function ($reason) use ($logger, $request, $formatter): PromiseInterface { + $response = $reason instanceof RequestException ? $reason->getResponse() : null; + $message = $formatter->format($request, $response, P\Create::exceptionFor($reason)); + $logger->error($message); + return P\Create::rejectionFor($reason); + } + ); + }; + }; + } + + /** + * This middleware adds a default content-type if possible, a default + * content-length or transfer-encoding header, and the expect header. + */ + public static function prepareBody(): callable + { + return static function (callable $handler): PrepareBodyMiddleware { + return new PrepareBodyMiddleware($handler); + }; + } + + /** + * Middleware that applies a map function to the request before passing to + * the next handler. + * + * @param callable $fn Function that accepts a RequestInterface and returns + * a RequestInterface. + */ + public static function mapRequest(callable $fn): callable + { + return static function (callable $handler) use ($fn): callable { + return static function (RequestInterface $request, array $options) use ($handler, $fn) { + return $handler($fn($request), $options); + }; + }; + } + + /** + * Middleware that applies a map function to the resolved promise's + * response. + * + * @param callable $fn Function that accepts a ResponseInterface and + * returns a ResponseInterface. + */ + public static function mapResponse(callable $fn): callable + { + return static function (callable $handler) use ($fn): callable { + return static function (RequestInterface $request, array $options) use ($handler, $fn) { + return $handler($request, $options)->then($fn); + }; + }; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Pool.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Pool.php new file mode 100644 index 0000000..6277c61 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Pool.php @@ -0,0 +1,125 @@ + $rfn) { + if ($rfn instanceof RequestInterface) { + yield $key => $client->sendAsync($rfn, $opts); + } elseif (\is_callable($rfn)) { + yield $key => $rfn($opts); + } else { + throw new \InvalidArgumentException('Each value yielded by the iterator must be a Psr7\Http\Message\RequestInterface or a callable that returns a promise that fulfills with a Psr7\Message\Http\ResponseInterface object.'); + } + } + }; + + $this->each = new EachPromise($requests(), $config); + } + + /** + * Get promise + */ + public function promise(): PromiseInterface + { + return $this->each->promise(); + } + + /** + * Sends multiple requests concurrently and returns an array of responses + * and exceptions that uses the same ordering as the provided requests. + * + * IMPORTANT: This method keeps every request and response in memory, and + * as such, is NOT recommended when sending a large number or an + * indeterminate number of requests concurrently. + * + * @param ClientInterface $client Client used to send the requests + * @param array|\Iterator $requests Requests to send concurrently. + * @param array $options Passes through the options available in + * {@see \GuzzleHttp\Pool::__construct} + * + * @return array Returns an array containing the response or an exception + * in the same order that the requests were sent. + * + * @throws \InvalidArgumentException if the event format is incorrect. + */ + public static function batch(ClientInterface $client, $requests, array $options = []): array + { + $res = []; + self::cmpCallback($options, 'fulfilled', $res); + self::cmpCallback($options, 'rejected', $res); + $pool = new static($client, $requests, $options); + $pool->promise()->wait(); + \ksort($res); + + return $res; + } + + /** + * Execute callback(s) + */ + private static function cmpCallback(array &$options, string $name, array &$results): void + { + if (!isset($options[$name])) { + $options[$name] = static function ($v, $k) use (&$results) { + $results[$k] = $v; + }; + } else { + $currentFn = $options[$name]; + $options[$name] = static function ($v, $k) use (&$results, $currentFn) { + $currentFn($v, $k); + $results[$k] = $v; + }; + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php new file mode 100644 index 0000000..7ca6283 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php @@ -0,0 +1,104 @@ +nextHandler = $nextHandler; + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + $fn = $this->nextHandler; + + // Don't do anything if the request has no body. + if ($request->getBody()->getSize() === 0) { + return $fn($request, $options); + } + + $modify = []; + + // Add a default content-type if possible. + if (!$request->hasHeader('Content-Type')) { + if ($uri = $request->getBody()->getMetadata('uri')) { + if (is_string($uri) && $type = Psr7\MimeType::fromFilename($uri)) { + $modify['set_headers']['Content-Type'] = $type; + } + } + } + + // Add a default content-length or transfer-encoding header. + if (!$request->hasHeader('Content-Length') + && !$request->hasHeader('Transfer-Encoding') + ) { + $size = $request->getBody()->getSize(); + if ($size !== null) { + $modify['set_headers']['Content-Length'] = $size; + } else { + $modify['set_headers']['Transfer-Encoding'] = 'chunked'; + } + } + + // Add the expect header if needed. + $this->addExpectHeader($request, $options, $modify); + + return $fn(Psr7\Utils::modifyRequest($request, $modify), $options); + } + + /** + * Add expect header + */ + private function addExpectHeader(RequestInterface $request, array $options, array &$modify): void + { + // Determine if the Expect header should be used + if ($request->hasHeader('Expect')) { + return; + } + + $expect = $options['expect'] ?? null; + + // Return if disabled or if you're not using HTTP/1.1 or HTTP/2.0 + if ($expect === false || $request->getProtocolVersion() < 1.1) { + return; + } + + // The expect header is unconditionally enabled + if ($expect === true) { + $modify['set_headers']['Expect'] = '100-Continue'; + return; + } + + // By default, send the expect header when the payload is > 1mb + if ($expect === null) { + $expect = 1048576; + } + + // Always add if the body cannot be rewound, the size cannot be + // determined, or the size is greater than the cutoff threshold + $body = $request->getBody(); + $size = $body->getSize(); + + if ($size === null || $size >= (int) $expect || !$body->isSeekable()) { + $modify['set_headers']['Expect'] = '100-Continue'; + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/RedirectMiddleware.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/RedirectMiddleware.php new file mode 100644 index 0000000..1dd3861 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/RedirectMiddleware.php @@ -0,0 +1,216 @@ + 5, + 'protocols' => ['http', 'https'], + 'strict' => false, + 'referer' => false, + 'track_redirects' => false, + ]; + + /** + * @var callable(RequestInterface, array): PromiseInterface + */ + private $nextHandler; + + /** + * @param callable(RequestInterface, array): PromiseInterface $nextHandler Next handler to invoke. + */ + public function __construct(callable $nextHandler) + { + $this->nextHandler = $nextHandler; + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + $fn = $this->nextHandler; + + if (empty($options['allow_redirects'])) { + return $fn($request, $options); + } + + if ($options['allow_redirects'] === true) { + $options['allow_redirects'] = self::$defaultSettings; + } elseif (!\is_array($options['allow_redirects'])) { + throw new \InvalidArgumentException('allow_redirects must be true, false, or array'); + } else { + // Merge the default settings with the provided settings + $options['allow_redirects'] += self::$defaultSettings; + } + + if (empty($options['allow_redirects']['max'])) { + return $fn($request, $options); + } + + return $fn($request, $options) + ->then(function (ResponseInterface $response) use ($request, $options) { + return $this->checkRedirect($request, $options, $response); + }); + } + + /** + * @return ResponseInterface|PromiseInterface + */ + public function checkRedirect(RequestInterface $request, array $options, ResponseInterface $response) + { + if (\strpos((string) $response->getStatusCode(), '3') !== 0 + || !$response->hasHeader('Location') + ) { + return $response; + } + + $this->guardMax($request, $response, $options); + $nextRequest = $this->modifyRequest($request, $options, $response); + + if (isset($options['allow_redirects']['on_redirect'])) { + ($options['allow_redirects']['on_redirect'])( + $request, + $response, + $nextRequest->getUri() + ); + } + + $promise = $this($nextRequest, $options); + + // Add headers to be able to track history of redirects. + if (!empty($options['allow_redirects']['track_redirects'])) { + return $this->withTracking( + $promise, + (string) $nextRequest->getUri(), + $response->getStatusCode() + ); + } + + return $promise; + } + + /** + * Enable tracking on promise. + */ + private function withTracking(PromiseInterface $promise, string $uri, int $statusCode): PromiseInterface + { + return $promise->then( + static function (ResponseInterface $response) use ($uri, $statusCode) { + // Note that we are pushing to the front of the list as this + // would be an earlier response than what is currently present + // in the history header. + $historyHeader = $response->getHeader(self::HISTORY_HEADER); + $statusHeader = $response->getHeader(self::STATUS_HISTORY_HEADER); + \array_unshift($historyHeader, $uri); + \array_unshift($statusHeader, (string) $statusCode); + + return $response->withHeader(self::HISTORY_HEADER, $historyHeader) + ->withHeader(self::STATUS_HISTORY_HEADER, $statusHeader); + } + ); + } + + /** + * Check for too many redirects + * + * @throws TooManyRedirectsException Too many redirects. + */ + private function guardMax(RequestInterface $request, ResponseInterface $response, array &$options): void + { + $current = $options['__redirect_count'] + ?? 0; + $options['__redirect_count'] = $current + 1; + $max = $options['allow_redirects']['max']; + + if ($options['__redirect_count'] > $max) { + throw new TooManyRedirectsException("Will not follow more than {$max} redirects", $request, $response); + } + } + + public function modifyRequest(RequestInterface $request, array $options, ResponseInterface $response): RequestInterface + { + // Request modifications to apply. + $modify = []; + $protocols = $options['allow_redirects']['protocols']; + + // Use a GET request if this is an entity enclosing request and we are + // not forcing RFC compliance, but rather emulating what all browsers + // would do. + $statusCode = $response->getStatusCode(); + if ($statusCode == 303 || + ($statusCode <= 302 && !$options['allow_redirects']['strict']) + ) { + $safeMethods = ['GET', 'HEAD', 'OPTIONS']; + $requestMethod = $request->getMethod(); + + $modify['method'] = in_array($requestMethod, $safeMethods) ? $requestMethod : 'GET'; + $modify['body'] = ''; + } + + $uri = $this->redirectUri($request, $response, $protocols); + if (isset($options['idn_conversion']) && ($options['idn_conversion'] !== false)) { + $idnOptions = ($options['idn_conversion'] === true) ? \IDNA_DEFAULT : $options['idn_conversion']; + $uri = Utils::idnUriConvert($uri, $idnOptions); + } + + $modify['uri'] = $uri; + Psr7\Message::rewindBody($request); + + // Add the Referer header if it is told to do so and only + // add the header if we are not redirecting from https to http. + if ($options['allow_redirects']['referer'] + && $modify['uri']->getScheme() === $request->getUri()->getScheme() + ) { + $uri = $request->getUri()->withUserInfo(''); + $modify['set_headers']['Referer'] = (string) $uri; + } else { + $modify['remove_headers'][] = 'Referer'; + } + + // Remove Authorization header if host is different. + if ($request->getUri()->getHost() !== $modify['uri']->getHost()) { + $modify['remove_headers'][] = 'Authorization'; + } + + return Psr7\Utils::modifyRequest($request, $modify); + } + + /** + * Set the appropriate URL on the request based on the location header + */ + private function redirectUri(RequestInterface $request, ResponseInterface $response, array $protocols): UriInterface + { + $location = Psr7\UriResolver::resolve( + $request->getUri(), + new Psr7\Uri($response->getHeaderLine('Location')) + ); + + // Ensure that the redirect URI is allowed based on the protocols. + if (!\in_array($location->getScheme(), $protocols)) { + throw new BadResponseException(\sprintf('Redirect URI, %s, does not use one of the allowed redirect protocols: %s', $location, \implode(', ', $protocols)), $request, $response); + } + + return $location; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/RequestOptions.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/RequestOptions.php new file mode 100644 index 0000000..20b31bc --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/RequestOptions.php @@ -0,0 +1,264 @@ +decider = $decider; + $this->nextHandler = $nextHandler; + $this->delay = $delay ?: __CLASS__ . '::exponentialDelay'; + } + + /** + * Default exponential backoff delay function. + * + * @return int milliseconds. + */ + public static function exponentialDelay(int $retries): int + { + return (int) \pow(2, $retries - 1) * 1000; + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + if (!isset($options['retries'])) { + $options['retries'] = 0; + } + + $fn = $this->nextHandler; + return $fn($request, $options) + ->then( + $this->onFulfilled($request, $options), + $this->onRejected($request, $options) + ); + } + + /** + * Execute fulfilled closure + */ + private function onFulfilled(RequestInterface $request, array $options): callable + { + return function ($value) use ($request, $options) { + if (!($this->decider)( + $options['retries'], + $request, + $value, + null + )) { + return $value; + } + return $this->doRetry($request, $options, $value); + }; + } + + /** + * Execute rejected closure + */ + private function onRejected(RequestInterface $req, array $options): callable + { + return function ($reason) use ($req, $options) { + if (!($this->decider)( + $options['retries'], + $req, + null, + $reason + )) { + return P\Create::rejectionFor($reason); + } + return $this->doRetry($req, $options); + }; + } + + private function doRetry(RequestInterface $request, array $options, ResponseInterface $response = null): PromiseInterface + { + $options['delay'] = ($this->delay)(++$options['retries'], $response); + + return $this($request, $options); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/TransferStats.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/TransferStats.php new file mode 100644 index 0000000..93fa334 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/TransferStats.php @@ -0,0 +1,133 @@ +request = $request; + $this->response = $response; + $this->transferTime = $transferTime; + $this->handlerErrorData = $handlerErrorData; + $this->handlerStats = $handlerStats; + } + + public function getRequest(): RequestInterface + { + return $this->request; + } + + /** + * Returns the response that was received (if any). + */ + public function getResponse(): ?ResponseInterface + { + return $this->response; + } + + /** + * Returns true if a response was received. + */ + public function hasResponse(): bool + { + return $this->response !== null; + } + + /** + * Gets handler specific error data. + * + * This might be an exception, a integer representing an error code, or + * anything else. Relying on this value assumes that you know what handler + * you are using. + * + * @return mixed + */ + public function getHandlerErrorData() + { + return $this->handlerErrorData; + } + + /** + * Get the effective URI the request was sent to. + */ + public function getEffectiveUri(): UriInterface + { + return $this->request->getUri(); + } + + /** + * Get the estimated time the request was being transferred by the handler. + * + * @return float|null Time in seconds. + */ + public function getTransferTime(): ?float + { + return $this->transferTime; + } + + /** + * Gets an array of all of the handler specific transfer data. + */ + public function getHandlerStats(): array + { + return $this->handlerStats; + } + + /** + * Get a specific handler statistic from the handler by name. + * + * @param string $stat Handler specific transfer stat to retrieve. + * + * @return mixed|null + */ + public function getHandlerStat(string $stat) + { + return $this->handlerStats[$stat] ?? null; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Utils.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Utils.php new file mode 100644 index 0000000..1e4e704 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/Utils.php @@ -0,0 +1,382 @@ +getHost()) { + $asciiHost = self::idnToAsci($uri->getHost(), $options, $info); + if ($asciiHost === false) { + $errorBitSet = $info['errors'] ?? 0; + + $errorConstants = array_filter(array_keys(get_defined_constants()), static function ($name) { + return substr($name, 0, 11) === 'IDNA_ERROR_'; + }); + + $errors = []; + foreach ($errorConstants as $errorConstant) { + if ($errorBitSet & constant($errorConstant)) { + $errors[] = $errorConstant; + } + } + + $errorMessage = 'IDN conversion failed'; + if ($errors) { + $errorMessage .= ' (errors: ' . implode(', ', $errors) . ')'; + } + + throw new InvalidArgumentException($errorMessage); + } + if ($uri->getHost() !== $asciiHost) { + // Replace URI only if the ASCII version is different + $uri = $uri->withHost($asciiHost); + } + } + + return $uri; + } + + /** + * @internal + */ + public static function getenv(string $name): ?string + { + if (isset($_SERVER[$name])) { + return (string) $_SERVER[$name]; + } + + if (\PHP_SAPI === 'cli' && ($value = \getenv($name)) !== false && $value !== null) { + return (string) $value; + } + + return null; + } + + /** + * @return string|false + */ + private static function idnToAsci(string $domain, int $options, ?array &$info = []) + { + if (\function_exists('idn_to_ascii') && \defined('INTL_IDNA_VARIANT_UTS46')) { + return \idn_to_ascii($domain, $options, \INTL_IDNA_VARIANT_UTS46, $info); + } + + throw new \Error('ext-idn or symfony/polyfill-intl-idn not loaded or too old'); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/functions.php b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/functions.php new file mode 100644 index 0000000..a70d2cb --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/guzzle/src/functions.php @@ -0,0 +1,167 @@ + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/Makefile b/user/plugins/automagic-images/vendor/guzzlehttp/promises/Makefile new file mode 100644 index 0000000..8d5b3ef --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/Makefile @@ -0,0 +1,13 @@ +all: clean test + +test: + vendor/bin/phpunit + +coverage: + vendor/bin/phpunit --coverage-html=artifacts/coverage + +view-coverage: + open artifacts/coverage/index.html + +clean: + rm -rf artifacts/* diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/README.md b/user/plugins/automagic-images/vendor/guzzlehttp/promises/README.md new file mode 100644 index 0000000..a95c605 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/README.md @@ -0,0 +1,532 @@ +# Guzzle Promises + +[Promises/A+](https://promisesaplus.com/) implementation that handles promise +chaining and resolution iteratively, allowing for "infinite" promise chaining +while keeping the stack size constant. Read [this blog post](https://blog.domenic.me/youre-missing-the-point-of-promises/) +for a general introduction to promises. + +- [Features](#features) +- [Quick start](#quick-start) +- [Synchronous wait](#synchronous-wait) +- [Cancellation](#cancellation) +- [API](#api) + - [Promise](#promise) + - [FulfilledPromise](#fulfilledpromise) + - [RejectedPromise](#rejectedpromise) +- [Promise interop](#promise-interop) +- [Implementation notes](#implementation-notes) + + +# Features + +- [Promises/A+](https://promisesaplus.com/) implementation. +- Promise resolution and chaining is handled iteratively, allowing for + "infinite" promise chaining. +- Promises have a synchronous `wait` method. +- Promises can be cancelled. +- Works with any object that has a `then` function. +- C# style async/await coroutine promises using + `GuzzleHttp\Promise\Coroutine::of()`. + + +# Quick start + +A *promise* represents the eventual result of an asynchronous operation. The +primary way of interacting with a promise is through its `then` method, which +registers callbacks to receive either a promise's eventual value or the reason +why the promise cannot be fulfilled. + + +## Callbacks + +Callbacks are registered with the `then` method by providing an optional +`$onFulfilled` followed by an optional `$onRejected` function. + + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise(); +$promise->then( + // $onFulfilled + function ($value) { + echo 'The promise was fulfilled.'; + }, + // $onRejected + function ($reason) { + echo 'The promise was rejected.'; + } +); +``` + +*Resolving* a promise means that you either fulfill a promise with a *value* or +reject a promise with a *reason*. Resolving a promises triggers callbacks +registered with the promises's `then` method. These callbacks are triggered +only once and in the order in which they were added. + + +## Resolving a promise + +Promises are fulfilled using the `resolve($value)` method. Resolving a promise +with any value other than a `GuzzleHttp\Promise\RejectedPromise` will trigger +all of the onFulfilled callbacks (resolving a promise with a rejected promise +will reject the promise and trigger the `$onRejected` callbacks). + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise(); +$promise + ->then(function ($value) { + // Return a value and don't break the chain + return "Hello, " . $value; + }) + // This then is executed after the first then and receives the value + // returned from the first then. + ->then(function ($value) { + echo $value; + }); + +// Resolving the promise triggers the $onFulfilled callbacks and outputs +// "Hello, reader." +$promise->resolve('reader.'); +``` + + +## Promise forwarding + +Promises can be chained one after the other. Each then in the chain is a new +promise. The return value of a promise is what's forwarded to the next +promise in the chain. Returning a promise in a `then` callback will cause the +subsequent promises in the chain to only be fulfilled when the returned promise +has been fulfilled. The next promise in the chain will be invoked with the +resolved value of the promise. + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise(); +$nextPromise = new Promise(); + +$promise + ->then(function ($value) use ($nextPromise) { + echo $value; + return $nextPromise; + }) + ->then(function ($value) { + echo $value; + }); + +// Triggers the first callback and outputs "A" +$promise->resolve('A'); +// Triggers the second callback and outputs "B" +$nextPromise->resolve('B'); +``` + +## Promise rejection + +When a promise is rejected, the `$onRejected` callbacks are invoked with the +rejection reason. + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise(); +$promise->then(null, function ($reason) { + echo $reason; +}); + +$promise->reject('Error!'); +// Outputs "Error!" +``` + +## Rejection forwarding + +If an exception is thrown in an `$onRejected` callback, subsequent +`$onRejected` callbacks are invoked with the thrown exception as the reason. + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise(); +$promise->then(null, function ($reason) { + throw new Exception($reason); +})->then(null, function ($reason) { + assert($reason->getMessage() === 'Error!'); +}); + +$promise->reject('Error!'); +``` + +You can also forward a rejection down the promise chain by returning a +`GuzzleHttp\Promise\RejectedPromise` in either an `$onFulfilled` or +`$onRejected` callback. + +```php +use GuzzleHttp\Promise\Promise; +use GuzzleHttp\Promise\RejectedPromise; + +$promise = new Promise(); +$promise->then(null, function ($reason) { + return new RejectedPromise($reason); +})->then(null, function ($reason) { + assert($reason === 'Error!'); +}); + +$promise->reject('Error!'); +``` + +If an exception is not thrown in a `$onRejected` callback and the callback +does not return a rejected promise, downstream `$onFulfilled` callbacks are +invoked using the value returned from the `$onRejected` callback. + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise(); +$promise + ->then(null, function ($reason) { + return "It's ok"; + }) + ->then(function ($value) { + assert($value === "It's ok"); + }); + +$promise->reject('Error!'); +``` + +# Synchronous wait + +You can synchronously force promises to complete using a promise's `wait` +method. When creating a promise, you can provide a wait function that is used +to synchronously force a promise to complete. When a wait function is invoked +it is expected to deliver a value to the promise or reject the promise. If the +wait function does not deliver a value, then an exception is thrown. The wait +function provided to a promise constructor is invoked when the `wait` function +of the promise is called. + +```php +$promise = new Promise(function () use (&$promise) { + $promise->resolve('foo'); +}); + +// Calling wait will return the value of the promise. +echo $promise->wait(); // outputs "foo" +``` + +If an exception is encountered while invoking the wait function of a promise, +the promise is rejected with the exception and the exception is thrown. + +```php +$promise = new Promise(function () use (&$promise) { + throw new Exception('foo'); +}); + +$promise->wait(); // throws the exception. +``` + +Calling `wait` on a promise that has been fulfilled will not trigger the wait +function. It will simply return the previously resolved value. + +```php +$promise = new Promise(function () { die('this is not called!'); }); +$promise->resolve('foo'); +echo $promise->wait(); // outputs "foo" +``` + +Calling `wait` on a promise that has been rejected will throw an exception. If +the rejection reason is an instance of `\Exception` the reason is thrown. +Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason +can be obtained by calling the `getReason` method of the exception. + +```php +$promise = new Promise(); +$promise->reject('foo'); +$promise->wait(); +``` + +> PHP Fatal error: Uncaught exception 'GuzzleHttp\Promise\RejectionException' with message 'The promise was rejected with value: foo' + + +## Unwrapping a promise + +When synchronously waiting on a promise, you are joining the state of the +promise into the current state of execution (i.e., return the value of the +promise if it was fulfilled or throw an exception if it was rejected). This is +called "unwrapping" the promise. Waiting on a promise will by default unwrap +the promise state. + +You can force a promise to resolve and *not* unwrap the state of the promise +by passing `false` to the first argument of the `wait` function: + +```php +$promise = new Promise(); +$promise->reject('foo'); +// This will not throw an exception. It simply ensures the promise has +// been resolved. +$promise->wait(false); +``` + +When unwrapping a promise, the resolved value of the promise will be waited +upon until the unwrapped value is not a promise. This means that if you resolve +promise A with a promise B and unwrap promise A, the value returned by the +wait function will be the value delivered to promise B. + +**Note**: when you do not unwrap the promise, no value is returned. + + +# Cancellation + +You can cancel a promise that has not yet been fulfilled using the `cancel()` +method of a promise. When creating a promise you can provide an optional +cancel function that when invoked cancels the action of computing a resolution +of the promise. + + +# API + + +## Promise + +When creating a promise object, you can provide an optional `$waitFn` and +`$cancelFn`. `$waitFn` is a function that is invoked with no arguments and is +expected to resolve the promise. `$cancelFn` is a function with no arguments +that is expected to cancel the computation of a promise. It is invoked when the +`cancel()` method of a promise is called. + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise( + function () use (&$promise) { + $promise->resolve('waited'); + }, + function () { + // do something that will cancel the promise computation (e.g., close + // a socket, cancel a database query, etc...) + } +); + +assert('waited' === $promise->wait()); +``` + +A promise has the following methods: + +- `then(callable $onFulfilled, callable $onRejected) : PromiseInterface` + + Appends fulfillment and rejection handlers to the promise, and returns a new promise resolving to the return value of the called handler. + +- `otherwise(callable $onRejected) : PromiseInterface` + + Appends a rejection handler callback to the promise, and returns a new promise resolving to the return value of the callback if it is called, or to its original fulfillment value if the promise is instead fulfilled. + +- `wait($unwrap = true) : mixed` + + Synchronously waits on the promise to complete. + + `$unwrap` controls whether or not the value of the promise is returned for a + fulfilled promise or if an exception is thrown if the promise is rejected. + This is set to `true` by default. + +- `cancel()` + + Attempts to cancel the promise if possible. The promise being cancelled and + the parent most ancestor that has not yet been resolved will also be + cancelled. Any promises waiting on the cancelled promise to resolve will also + be cancelled. + +- `getState() : string` + + Returns the state of the promise. One of `pending`, `fulfilled`, or + `rejected`. + +- `resolve($value)` + + Fulfills the promise with the given `$value`. + +- `reject($reason)` + + Rejects the promise with the given `$reason`. + + +## FulfilledPromise + +A fulfilled promise can be created to represent a promise that has been +fulfilled. + +```php +use GuzzleHttp\Promise\FulfilledPromise; + +$promise = new FulfilledPromise('value'); + +// Fulfilled callbacks are immediately invoked. +$promise->then(function ($value) { + echo $value; +}); +``` + + +## RejectedPromise + +A rejected promise can be created to represent a promise that has been +rejected. + +```php +use GuzzleHttp\Promise\RejectedPromise; + +$promise = new RejectedPromise('Error'); + +// Rejected callbacks are immediately invoked. +$promise->then(null, function ($reason) { + echo $reason; +}); +``` + + +# Promise interop + +This library works with foreign promises that have a `then` method. This means +you can use Guzzle promises with [React promises](https://github.com/reactphp/promise) +for example. When a foreign promise is returned inside of a then method +callback, promise resolution will occur recursively. + +```php +// Create a React promise +$deferred = new React\Promise\Deferred(); +$reactPromise = $deferred->promise(); + +// Create a Guzzle promise that is fulfilled with a React promise. +$guzzlePromise = new GuzzleHttp\Promise\Promise(); +$guzzlePromise->then(function ($value) use ($reactPromise) { + // Do something something with the value... + // Return the React promise + return $reactPromise; +}); +``` + +Please note that wait and cancel chaining is no longer possible when forwarding +a foreign promise. You will need to wrap a third-party promise with a Guzzle +promise in order to utilize wait and cancel functions with foreign promises. + + +## Event Loop Integration + +In order to keep the stack size constant, Guzzle promises are resolved +asynchronously using a task queue. When waiting on promises synchronously, the +task queue will be automatically run to ensure that the blocking promise and +any forwarded promises are resolved. When using promises asynchronously in an +event loop, you will need to run the task queue on each tick of the loop. If +you do not run the task queue, then promises will not be resolved. + +You can run the task queue using the `run()` method of the global task queue +instance. + +```php +// Get the global task queue +$queue = GuzzleHttp\Promise\Utils::queue(); +$queue->run(); +``` + +For example, you could use Guzzle promises with React using a periodic timer: + +```php +$loop = React\EventLoop\Factory::create(); +$loop->addPeriodicTimer(0, [$queue, 'run']); +``` + +*TODO*: Perhaps adding a `futureTick()` on each tick would be faster? + + +# Implementation notes + + +## Promise resolution and chaining is handled iteratively + +By shuffling pending handlers from one owner to another, promises are +resolved iteratively, allowing for "infinite" then chaining. + +```php +then(function ($v) { + // The stack size remains constant (a good thing) + echo xdebug_get_stack_depth() . ', '; + return $v + 1; + }); +} + +$parent->resolve(0); +var_dump($p->wait()); // int(1000) + +``` + +When a promise is fulfilled or rejected with a non-promise value, the promise +then takes ownership of the handlers of each child promise and delivers values +down the chain without using recursion. + +When a promise is resolved with another promise, the original promise transfers +all of its pending handlers to the new promise. When the new promise is +eventually resolved, all of the pending handlers are delivered the forwarded +value. + + +## A promise is the deferred. + +Some promise libraries implement promises using a deferred object to represent +a computation and a promise object to represent the delivery of the result of +the computation. This is a nice separation of computation and delivery because +consumers of the promise cannot modify the value that will be eventually +delivered. + +One side effect of being able to implement promise resolution and chaining +iteratively is that you need to be able for one promise to reach into the state +of another promise to shuffle around ownership of handlers. In order to achieve +this without making the handlers of a promise publicly mutable, a promise is +also the deferred value, allowing promises of the same parent class to reach +into and modify the private properties of promises of the same type. While this +does allow consumers of the value to modify the resolution or rejection of the +deferred, it is a small price to pay for keeping the stack size constant. + +```php +$promise = new Promise(); +$promise->then(function ($value) { echo $value; }); +// The promise is the deferred value, so you can deliver a value to it. +$promise->resolve('foo'); +// prints "foo" +``` + + +## Upgrading from Function API + +A static API was first introduced in 1.4.0, in order to mitigate problems with functions conflicting between global and local copies of the package. The function API will be removed in 2.0.0. A migration table has been provided here for your convenience: + +| Original Function | Replacement Method | +|----------------|----------------| +| `queue` | `Utils::queue` | +| `task` | `Utils::task` | +| `promise_for` | `Create::promiseFor` | +| `rejection_for` | `Create::rejectionFor` | +| `exception_for` | `Create::exceptionFor` | +| `iter_for` | `Create::iterFor` | +| `inspect` | `Utils::inspect` | +| `inspect_all` | `Utils::inspectAll` | +| `unwrap` | `Utils::unwrap` | +| `all` | `Utils::all` | +| `some` | `Utils::some` | +| `any` | `Utils::any` | +| `settle` | `Utils::settle` | +| `each` | `Each::of` | +| `each_limit` | `Each::ofLimit` | +| `each_limit_all` | `Each::ofLimitAll` | +| `!is_fulfilled` | `Is::pending` | +| `is_fulfilled` | `Is::fulfilled` | +| `is_rejected` | `Is::rejected` | +| `is_settled` | `Is::settled` | +| `coroutine` | `Coroutine::of` | diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/composer.json b/user/plugins/automagic-images/vendor/guzzlehttp/promises/composer.json new file mode 100644 index 0000000..db44d9e --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/composer.json @@ -0,0 +1,39 @@ +{ + "name": "guzzlehttp/promises", + "description": "Guzzle promises library", + "keywords": ["promise"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": ["src/functions_include.php"] + }, + "autoload-dev": { + "psr-4": { + "GuzzleHttp\\Promise\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "vendor/bin/simple-phpunit", + "test-ci": "vendor/bin/simple-phpunit --coverage-text" + }, + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/AggregateException.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/AggregateException.php new file mode 100644 index 0000000..d2b5712 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/AggregateException.php @@ -0,0 +1,17 @@ +then(function ($v) { echo $v; }); + * + * @param callable $generatorFn Generator function to wrap into a promise. + * + * @return Promise + * + * @link https://github.com/petkaantonov/bluebird/blob/master/API.md#generators inspiration + */ +final class Coroutine implements PromiseInterface +{ + /** + * @var PromiseInterface|null + */ + private $currentPromise; + + /** + * @var Generator + */ + private $generator; + + /** + * @var Promise + */ + private $result; + + public function __construct(callable $generatorFn) + { + $this->generator = $generatorFn(); + $this->result = new Promise(function () { + while (isset($this->currentPromise)) { + $this->currentPromise->wait(); + } + }); + try { + $this->nextCoroutine($this->generator->current()); + } catch (\Exception $exception) { + $this->result->reject($exception); + } catch (Throwable $throwable) { + $this->result->reject($throwable); + } + } + + /** + * Create a new coroutine. + * + * @return self + */ + public static function of(callable $generatorFn) + { + return new self($generatorFn); + } + + public function then( + callable $onFulfilled = null, + callable $onRejected = null + ) { + return $this->result->then($onFulfilled, $onRejected); + } + + public function otherwise(callable $onRejected) + { + return $this->result->otherwise($onRejected); + } + + public function wait($unwrap = true) + { + return $this->result->wait($unwrap); + } + + public function getState() + { + return $this->result->getState(); + } + + public function resolve($value) + { + $this->result->resolve($value); + } + + public function reject($reason) + { + $this->result->reject($reason); + } + + public function cancel() + { + $this->currentPromise->cancel(); + $this->result->cancel(); + } + + private function nextCoroutine($yielded) + { + $this->currentPromise = Create::promiseFor($yielded) + ->then([$this, '_handleSuccess'], [$this, '_handleFailure']); + } + + /** + * @internal + */ + public function _handleSuccess($value) + { + unset($this->currentPromise); + try { + $next = $this->generator->send($value); + if ($this->generator->valid()) { + $this->nextCoroutine($next); + } else { + $this->result->resolve($value); + } + } catch (Exception $exception) { + $this->result->reject($exception); + } catch (Throwable $throwable) { + $this->result->reject($throwable); + } + } + + /** + * @internal + */ + public function _handleFailure($reason) + { + unset($this->currentPromise); + try { + $nextYield = $this->generator->throw(Create::exceptionFor($reason)); + // The throw was caught, so keep iterating on the coroutine + $this->nextCoroutine($nextYield); + } catch (Exception $exception) { + $this->result->reject($exception); + } catch (Throwable $throwable) { + $this->result->reject($throwable); + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Create.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Create.php new file mode 100644 index 0000000..8d038e9 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Create.php @@ -0,0 +1,84 @@ +then([$promise, 'resolve'], [$promise, 'reject']); + return $promise; + } + + return new FulfilledPromise($value); + } + + /** + * Creates a rejected promise for a reason if the reason is not a promise. + * If the provided reason is a promise, then it is returned as-is. + * + * @param mixed $reason Promise or reason. + * + * @return PromiseInterface + */ + public static function rejectionFor($reason) + { + if ($reason instanceof PromiseInterface) { + return $reason; + } + + return new RejectedPromise($reason); + } + + /** + * Create an exception for a rejected promise value. + * + * @param mixed $reason + * + * @return \Exception|\Throwable + */ + public static function exceptionFor($reason) + { + if ($reason instanceof \Exception || $reason instanceof \Throwable) { + return $reason; + } + + return new RejectionException($reason); + } + + /** + * Returns an iterator for the given value. + * + * @param mixed $value + * + * @return \Iterator + */ + public static function iterFor($value) + { + if ($value instanceof \Iterator) { + return $value; + } + + if (is_array($value)) { + return new \ArrayIterator($value); + } + + return new \ArrayIterator([$value]); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Each.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Each.php new file mode 100644 index 0000000..1dda354 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Each.php @@ -0,0 +1,90 @@ + $onFulfilled, + 'rejected' => $onRejected + ]))->promise(); + } + + /** + * Like of, but only allows a certain number of outstanding promises at any + * given time. + * + * $concurrency may be an integer or a function that accepts the number of + * pending promises and returns a numeric concurrency limit value to allow + * for dynamic a concurrency size. + * + * @param mixed $iterable + * @param int|callable $concurrency + * @param callable $onFulfilled + * @param callable $onRejected + * + * @return PromiseInterface + */ + public static function ofLimit( + $iterable, + $concurrency, + callable $onFulfilled = null, + callable $onRejected = null + ) { + return (new EachPromise($iterable, [ + 'fulfilled' => $onFulfilled, + 'rejected' => $onRejected, + 'concurrency' => $concurrency + ]))->promise(); + } + + /** + * Like limit, but ensures that no promise in the given $iterable argument + * is rejected. If any promise is rejected, then the aggregate promise is + * rejected with the encountered rejection. + * + * @param mixed $iterable + * @param int|callable $concurrency + * @param callable $onFulfilled + * + * @return PromiseInterface + */ + public static function ofLimitAll( + $iterable, + $concurrency, + callable $onFulfilled = null + ) { + return each_limit( + $iterable, + $concurrency, + $onFulfilled, + function ($reason, $idx, PromiseInterface $aggregate) { + $aggregate->reject($reason); + } + ); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/EachPromise.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/EachPromise.php new file mode 100644 index 0000000..748f471 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/EachPromise.php @@ -0,0 +1,254 @@ +iterable = Create::iterFor($iterable); + + if (isset($config['concurrency'])) { + $this->concurrency = $config['concurrency']; + } + + if (isset($config['fulfilled'])) { + $this->onFulfilled = $config['fulfilled']; + } + + if (isset($config['rejected'])) { + $this->onRejected = $config['rejected']; + } + } + + /** @psalm-suppress InvalidNullableReturnType */ + public function promise() + { + if ($this->aggregate) { + return $this->aggregate; + } + + try { + $this->createPromise(); + /** @psalm-assert Promise $this->aggregate */ + $this->iterable->rewind(); + if (!$this->checkIfFinished()) { + $this->refillPending(); + } + } catch (\Throwable $e) { + /** + * @psalm-suppress NullReference + * @phpstan-ignore-next-line + */ + $this->aggregate->reject($e); + } catch (\Exception $e) { + /** + * @psalm-suppress NullReference + * @phpstan-ignore-next-line + */ + $this->aggregate->reject($e); + } + + /** + * @psalm-suppress NullableReturnStatement + * @phpstan-ignore-next-line + */ + return $this->aggregate; + } + + private function createPromise() + { + $this->mutex = false; + $this->aggregate = new Promise(function () { + reset($this->pending); + // Consume a potentially fluctuating list of promises while + // ensuring that indexes are maintained (precluding array_shift). + while ($promise = current($this->pending)) { + next($this->pending); + $promise->wait(); + if (Is::settled($this->aggregate)) { + return; + } + } + }); + + // Clear the references when the promise is resolved. + $clearFn = function () { + $this->iterable = $this->concurrency = $this->pending = null; + $this->onFulfilled = $this->onRejected = null; + $this->nextPendingIndex = 0; + }; + + $this->aggregate->then($clearFn, $clearFn); + } + + private function refillPending() + { + if (!$this->concurrency) { + // Add all pending promises. + while ($this->addPending() && $this->advanceIterator()); + return; + } + + // Add only up to N pending promises. + $concurrency = is_callable($this->concurrency) + ? call_user_func($this->concurrency, count($this->pending)) + : $this->concurrency; + $concurrency = max($concurrency - count($this->pending), 0); + // Concurrency may be set to 0 to disallow new promises. + if (!$concurrency) { + return; + } + // Add the first pending promise. + $this->addPending(); + // Note this is special handling for concurrency=1 so that we do + // not advance the iterator after adding the first promise. This + // helps work around issues with generators that might not have the + // next value to yield until promise callbacks are called. + while (--$concurrency + && $this->advanceIterator() + && $this->addPending()); + } + + private function addPending() + { + if (!$this->iterable || !$this->iterable->valid()) { + return false; + } + + $promise = Create::promiseFor($this->iterable->current()); + $key = $this->iterable->key(); + + // Iterable keys may not be unique, so we use a counter to + // guarantee uniqueness + $idx = $this->nextPendingIndex++; + + $this->pending[$idx] = $promise->then( + function ($value) use ($idx, $key) { + if ($this->onFulfilled) { + call_user_func( + $this->onFulfilled, + $value, + $key, + $this->aggregate + ); + } + $this->step($idx); + }, + function ($reason) use ($idx, $key) { + if ($this->onRejected) { + call_user_func( + $this->onRejected, + $reason, + $key, + $this->aggregate + ); + } + $this->step($idx); + } + ); + + return true; + } + + private function advanceIterator() + { + // Place a lock on the iterator so that we ensure to not recurse, + // preventing fatal generator errors. + if ($this->mutex) { + return false; + } + + $this->mutex = true; + + try { + $this->iterable->next(); + $this->mutex = false; + return true; + } catch (\Throwable $e) { + $this->aggregate->reject($e); + $this->mutex = false; + return false; + } catch (\Exception $e) { + $this->aggregate->reject($e); + $this->mutex = false; + return false; + } + } + + private function step($idx) + { + // If the promise was already resolved, then ignore this step. + if (Is::settled($this->aggregate)) { + return; + } + + unset($this->pending[$idx]); + + // Only refill pending promises if we are not locked, preventing the + // EachPromise to recursively invoke the provided iterator, which + // cause a fatal error: "Cannot resume an already running generator" + if ($this->advanceIterator() && !$this->checkIfFinished()) { + // Add more pending promises if possible. + $this->refillPending(); + } + } + + private function checkIfFinished() + { + if (!$this->pending && !$this->iterable->valid()) { + // Resolve the promise if there's nothing left to do. + $this->aggregate->resolve(null); + return true; + } + + return false; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/FulfilledPromise.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/FulfilledPromise.php new file mode 100644 index 0000000..98f72a6 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/FulfilledPromise.php @@ -0,0 +1,84 @@ +value = $value; + } + + public function then( + callable $onFulfilled = null, + callable $onRejected = null + ) { + // Return itself if there is no onFulfilled function. + if (!$onFulfilled) { + return $this; + } + + $queue = Utils::queue(); + $p = new Promise([$queue, 'run']); + $value = $this->value; + $queue->add(static function () use ($p, $value, $onFulfilled) { + if (Is::pending($p)) { + try { + $p->resolve($onFulfilled($value)); + } catch (\Throwable $e) { + $p->reject($e); + } catch (\Exception $e) { + $p->reject($e); + } + } + }); + + return $p; + } + + public function otherwise(callable $onRejected) + { + return $this->then(null, $onRejected); + } + + public function wait($unwrap = true, $defaultDelivery = null) + { + return $unwrap ? $this->value : null; + } + + public function getState() + { + return self::FULFILLED; + } + + public function resolve($value) + { + if ($value !== $this->value) { + throw new \LogicException("Cannot resolve a fulfilled promise"); + } + } + + public function reject($reason) + { + throw new \LogicException("Cannot reject a fulfilled promise"); + } + + public function cancel() + { + // pass + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Is.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Is.php new file mode 100644 index 0000000..c3ed8d0 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Is.php @@ -0,0 +1,46 @@ +getState() === PromiseInterface::PENDING; + } + + /** + * Returns true if a promise is fulfilled or rejected. + * + * @return bool + */ + public static function settled(PromiseInterface $promise) + { + return $promise->getState() !== PromiseInterface::PENDING; + } + + /** + * Returns true if a promise is fulfilled. + * + * @return bool + */ + public static function fulfilled(PromiseInterface $promise) + { + return $promise->getState() === PromiseInterface::FULFILLED; + } + + /** + * Returns true if a promise is rejected. + * + * @return bool + */ + public static function rejected(PromiseInterface $promise) + { + return $promise->getState() === PromiseInterface::REJECTED; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Promise.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Promise.php new file mode 100644 index 0000000..7593905 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/Promise.php @@ -0,0 +1,278 @@ +waitFn = $waitFn; + $this->cancelFn = $cancelFn; + } + + public function then( + callable $onFulfilled = null, + callable $onRejected = null + ) { + if ($this->state === self::PENDING) { + $p = new Promise(null, [$this, 'cancel']); + $this->handlers[] = [$p, $onFulfilled, $onRejected]; + $p->waitList = $this->waitList; + $p->waitList[] = $this; + return $p; + } + + // Return a fulfilled promise and immediately invoke any callbacks. + if ($this->state === self::FULFILLED) { + $promise = Create::promiseFor($this->result); + return $onFulfilled ? $promise->then($onFulfilled) : $promise; + } + + // It's either cancelled or rejected, so return a rejected promise + // and immediately invoke any callbacks. + $rejection = Create::rejectionFor($this->result); + return $onRejected ? $rejection->then(null, $onRejected) : $rejection; + } + + public function otherwise(callable $onRejected) + { + return $this->then(null, $onRejected); + } + + public function wait($unwrap = true) + { + $this->waitIfPending(); + + if ($this->result instanceof PromiseInterface) { + return $this->result->wait($unwrap); + } + if ($unwrap) { + if ($this->state === self::FULFILLED) { + return $this->result; + } + // It's rejected so "unwrap" and throw an exception. + throw Create::exceptionFor($this->result); + } + } + + public function getState() + { + return $this->state; + } + + public function cancel() + { + if ($this->state !== self::PENDING) { + return; + } + + $this->waitFn = $this->waitList = null; + + if ($this->cancelFn) { + $fn = $this->cancelFn; + $this->cancelFn = null; + try { + $fn(); + } catch (\Throwable $e) { + $this->reject($e); + } catch (\Exception $e) { + $this->reject($e); + } + } + + // Reject the promise only if it wasn't rejected in a then callback. + /** @psalm-suppress RedundantCondition */ + if ($this->state === self::PENDING) { + $this->reject(new CancellationException('Promise has been cancelled')); + } + } + + public function resolve($value) + { + $this->settle(self::FULFILLED, $value); + } + + public function reject($reason) + { + $this->settle(self::REJECTED, $reason); + } + + private function settle($state, $value) + { + if ($this->state !== self::PENDING) { + // Ignore calls with the same resolution. + if ($state === $this->state && $value === $this->result) { + return; + } + throw $this->state === $state + ? new \LogicException("The promise is already {$state}.") + : new \LogicException("Cannot change a {$this->state} promise to {$state}"); + } + + if ($value === $this) { + throw new \LogicException('Cannot fulfill or reject a promise with itself'); + } + + // Clear out the state of the promise but stash the handlers. + $this->state = $state; + $this->result = $value; + $handlers = $this->handlers; + $this->handlers = null; + $this->waitList = $this->waitFn = null; + $this->cancelFn = null; + + if (!$handlers) { + return; + } + + // If the value was not a settled promise or a thenable, then resolve + // it in the task queue using the correct ID. + if (!is_object($value) || !method_exists($value, 'then')) { + $id = $state === self::FULFILLED ? 1 : 2; + // It's a success, so resolve the handlers in the queue. + Utils::queue()->add(static function () use ($id, $value, $handlers) { + foreach ($handlers as $handler) { + self::callHandler($id, $value, $handler); + } + }); + } elseif ($value instanceof Promise && Is::pending($value)) { + // We can just merge our handlers onto the next promise. + $value->handlers = array_merge($value->handlers, $handlers); + } else { + // Resolve the handlers when the forwarded promise is resolved. + $value->then( + static function ($value) use ($handlers) { + foreach ($handlers as $handler) { + self::callHandler(1, $value, $handler); + } + }, + static function ($reason) use ($handlers) { + foreach ($handlers as $handler) { + self::callHandler(2, $reason, $handler); + } + } + ); + } + } + + /** + * Call a stack of handlers using a specific callback index and value. + * + * @param int $index 1 (resolve) or 2 (reject). + * @param mixed $value Value to pass to the callback. + * @param array $handler Array of handler data (promise and callbacks). + */ + private static function callHandler($index, $value, array $handler) + { + /** @var PromiseInterface $promise */ + $promise = $handler[0]; + + // The promise may have been cancelled or resolved before placing + // this thunk in the queue. + if (Is::settled($promise)) { + return; + } + + try { + if (isset($handler[$index])) { + /* + * If $f throws an exception, then $handler will be in the exception + * stack trace. Since $handler contains a reference to the callable + * itself we get a circular reference. We clear the $handler + * here to avoid that memory leak. + */ + $f = $handler[$index]; + unset($handler); + $promise->resolve($f($value)); + } elseif ($index === 1) { + // Forward resolution values as-is. + $promise->resolve($value); + } else { + // Forward rejections down the chain. + $promise->reject($value); + } + } catch (\Throwable $reason) { + $promise->reject($reason); + } catch (\Exception $reason) { + $promise->reject($reason); + } + } + + private function waitIfPending() + { + if ($this->state !== self::PENDING) { + return; + } elseif ($this->waitFn) { + $this->invokeWaitFn(); + } elseif ($this->waitList) { + $this->invokeWaitList(); + } else { + // If there's no wait function, then reject the promise. + $this->reject('Cannot wait on a promise that has ' + . 'no internal wait function. You must provide a wait ' + . 'function when constructing the promise to be able to ' + . 'wait on a promise.'); + } + + Utils::queue()->run(); + + /** @psalm-suppress RedundantCondition */ + if ($this->state === self::PENDING) { + $this->reject('Invoking the wait callback did not resolve the promise'); + } + } + + private function invokeWaitFn() + { + try { + $wfn = $this->waitFn; + $this->waitFn = null; + $wfn(true); + } catch (\Exception $reason) { + if ($this->state === self::PENDING) { + // The promise has not been resolved yet, so reject the promise + // with the exception. + $this->reject($reason); + } else { + // The promise was already resolved, so there's a problem in + // the application. + throw $reason; + } + } + } + + private function invokeWaitList() + { + $waitList = $this->waitList; + $this->waitList = null; + + foreach ($waitList as $result) { + do { + $result->waitIfPending(); + $result = $result->result; + } while ($result instanceof Promise); + + if ($result instanceof PromiseInterface) { + $result->wait(false); + } + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/PromiseInterface.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/PromiseInterface.php new file mode 100644 index 0000000..e598331 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/PromiseInterface.php @@ -0,0 +1,97 @@ +reason = $reason; + } + + public function then( + callable $onFulfilled = null, + callable $onRejected = null + ) { + // If there's no onRejected callback then just return self. + if (!$onRejected) { + return $this; + } + + $queue = Utils::queue(); + $reason = $this->reason; + $p = new Promise([$queue, 'run']); + $queue->add(static function () use ($p, $reason, $onRejected) { + if (Is::pending($p)) { + try { + // Return a resolved promise if onRejected does not throw. + $p->resolve($onRejected($reason)); + } catch (\Throwable $e) { + // onRejected threw, so return a rejected promise. + $p->reject($e); + } catch (\Exception $e) { + // onRejected threw, so return a rejected promise. + $p->reject($e); + } + } + }); + + return $p; + } + + public function otherwise(callable $onRejected) + { + return $this->then(null, $onRejected); + } + + public function wait($unwrap = true, $defaultDelivery = null) + { + if ($unwrap) { + throw Create::exceptionFor($this->reason); + } + + return null; + } + + public function getState() + { + return self::REJECTED; + } + + public function resolve($value) + { + throw new \LogicException("Cannot resolve a rejected promise"); + } + + public function reject($reason) + { + if ($reason !== $this->reason) { + throw new \LogicException("Cannot reject a rejected promise"); + } + } + + public function cancel() + { + // pass + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/RejectionException.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/RejectionException.php new file mode 100644 index 0000000..e2f1377 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/RejectionException.php @@ -0,0 +1,48 @@ +reason = $reason; + + $message = 'The promise was rejected'; + + if ($description) { + $message .= ' with reason: ' . $description; + } elseif (is_string($reason) + || (is_object($reason) && method_exists($reason, '__toString')) + ) { + $message .= ' with reason: ' . $this->reason; + } elseif ($reason instanceof \JsonSerializable) { + $message .= ' with reason: ' + . json_encode($this->reason, JSON_PRETTY_PRINT); + } + + parent::__construct($message); + } + + /** + * Returns the rejection reason. + * + * @return mixed + */ + public function getReason() + { + return $this->reason; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/TaskQueue.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/TaskQueue.php new file mode 100644 index 0000000..f0fba2c --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/TaskQueue.php @@ -0,0 +1,67 @@ +run(); + */ +class TaskQueue implements TaskQueueInterface +{ + private $enableShutdown = true; + private $queue = []; + + public function __construct($withShutdown = true) + { + if ($withShutdown) { + register_shutdown_function(function () { + if ($this->enableShutdown) { + // Only run the tasks if an E_ERROR didn't occur. + $err = error_get_last(); + if (!$err || ($err['type'] ^ E_ERROR)) { + $this->run(); + } + } + }); + } + } + + public function isEmpty() + { + return !$this->queue; + } + + public function add(callable $task) + { + $this->queue[] = $task; + } + + public function run() + { + while ($task = array_shift($this->queue)) { + /** @var callable $task */ + $task(); + } + } + + /** + * The task queue will be run and exhausted by default when the process + * exits IFF the exit is not the result of a PHP E_ERROR error. + * + * You can disable running the automatic shutdown of the queue by calling + * this function. If you disable the task queue shutdown process, then you + * MUST either run the task queue (as a result of running your event loop + * or manually using the run() method) or wait on each outstanding promise. + * + * Note: This shutdown will occur before any destructors are triggered. + */ + public function disableShutdown() + { + $this->enableShutdown = false; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/TaskQueueInterface.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/TaskQueueInterface.php new file mode 100644 index 0000000..723d4d5 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/TaskQueueInterface.php @@ -0,0 +1,24 @@ + + * while ($eventLoop->isRunning()) { + * GuzzleHttp\Promise\Utils::queue()->run(); + * } + * + * + * @param TaskQueueInterface $assign Optionally specify a new queue instance. + * + * @return TaskQueueInterface + */ + public static function queue(TaskQueueInterface $assign = null) + { + static $queue; + + if ($assign) { + $queue = $assign; + } elseif (!$queue) { + $queue = new TaskQueue(); + } + + return $queue; + } + + /** + * Adds a function to run in the task queue when it is next `run()` and + * returns a promise that is fulfilled or rejected with the result. + * + * @param callable $task Task function to run. + * + * @return PromiseInterface + */ + public static function task(callable $task) + { + $queue = self::queue(); + $promise = new Promise([$queue, 'run']); + $queue->add(function () use ($task, $promise) { + try { + $promise->resolve($task()); + } catch (\Throwable $e) { + $promise->reject($e); + } catch (\Exception $e) { + $promise->reject($e); + } + }); + + return $promise; + } + + /** + * Synchronously waits on a promise to resolve and returns an inspection + * state array. + * + * Returns a state associative array containing a "state" key mapping to a + * valid promise state. If the state of the promise is "fulfilled", the + * array will contain a "value" key mapping to the fulfilled value of the + * promise. If the promise is rejected, the array will contain a "reason" + * key mapping to the rejection reason of the promise. + * + * @param PromiseInterface $promise Promise or value. + * + * @return array + */ + public static function inspect(PromiseInterface $promise) + { + try { + return [ + 'state' => PromiseInterface::FULFILLED, + 'value' => $promise->wait() + ]; + } catch (RejectionException $e) { + return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()]; + } catch (\Throwable $e) { + return ['state' => PromiseInterface::REJECTED, 'reason' => $e]; + } catch (\Exception $e) { + return ['state' => PromiseInterface::REJECTED, 'reason' => $e]; + } + } + + /** + * Waits on all of the provided promises, but does not unwrap rejected + * promises as thrown exception. + * + * Returns an array of inspection state arrays. + * + * @see inspect for the inspection state array format. + * + * @param PromiseInterface[] $promises Traversable of promises to wait upon. + * + * @return array + */ + public static function inspectAll($promises) + { + $results = []; + foreach ($promises as $key => $promise) { + $results[$key] = inspect($promise); + } + + return $results; + } + + /** + * Waits on all of the provided promises and returns the fulfilled values. + * + * Returns an array that contains the value of each promise (in the same + * order the promises were provided). An exception is thrown if any of the + * promises are rejected. + * + * @param iterable $promises Iterable of PromiseInterface objects to wait on. + * + * @return array + * + * @throws \Exception on error + * @throws \Throwable on error in PHP >=7 + */ + public static function unwrap($promises) + { + $results = []; + foreach ($promises as $key => $promise) { + $results[$key] = $promise->wait(); + } + + return $results; + } + + /** + * Given an array of promises, return a promise that is fulfilled when all + * the items in the array are fulfilled. + * + * The promise's fulfillment value is an array with fulfillment values at + * respective positions to the original array. If any promise in the array + * rejects, the returned promise is rejected with the rejection reason. + * + * @param mixed $promises Promises or values. + * @param bool $recursive If true, resolves new promises that might have been added to the stack during its own resolution. + * + * @return PromiseInterface + */ + public static function all($promises, $recursive = false) + { + $results = []; + $promise = Each::of( + $promises, + function ($value, $idx) use (&$results) { + $results[$idx] = $value; + }, + function ($reason, $idx, Promise $aggregate) { + $aggregate->reject($reason); + } + )->then(function () use (&$results) { + ksort($results); + return $results; + }); + + if (true === $recursive) { + $promise = $promise->then(function ($results) use ($recursive, &$promises) { + foreach ($promises as $promise) { + if (Is::pending($promise)) { + return self::all($promises, $recursive); + } + } + return $results; + }); + } + + return $promise; + } + + /** + * Initiate a competitive race between multiple promises or values (values + * will become immediately fulfilled promises). + * + * When count amount of promises have been fulfilled, the returned promise + * is fulfilled with an array that contains the fulfillment values of the + * winners in order of resolution. + * + * This promise is rejected with a {@see AggregateException} if the number + * of fulfilled promises is less than the desired $count. + * + * @param int $count Total number of promises. + * @param mixed $promises Promises or values. + * + * @return PromiseInterface + */ + public static function some($count, $promises) + { + $results = []; + $rejections = []; + + return Each::of( + $promises, + function ($value, $idx, PromiseInterface $p) use (&$results, $count) { + if (Is::settled($p)) { + return; + } + $results[$idx] = $value; + if (count($results) >= $count) { + $p->resolve(null); + } + }, + function ($reason) use (&$rejections) { + $rejections[] = $reason; + } + )->then( + function () use (&$results, &$rejections, $count) { + if (count($results) !== $count) { + throw new AggregateException( + 'Not enough promises to fulfill count', + $rejections + ); + } + ksort($results); + return array_values($results); + } + ); + } + + /** + * Like some(), with 1 as count. However, if the promise fulfills, the + * fulfillment value is not an array of 1 but the value directly. + * + * @param mixed $promises Promises or values. + * + * @return PromiseInterface + */ + public static function any($promises) + { + return self::some(1, $promises)->then(function ($values) { + return $values[0]; + }); + } + + /** + * Returns a promise that is fulfilled when all of the provided promises have + * been fulfilled or rejected. + * + * The returned promise is fulfilled with an array of inspection state arrays. + * + * @see inspect for the inspection state array format. + * + * @param mixed $promises Promises or values. + * + * @return PromiseInterface + */ + public static function settle($promises) + { + $results = []; + + return Each::of( + $promises, + function ($value, $idx) use (&$results) { + $results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value]; + }, + function ($reason, $idx) use (&$results) { + $results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason]; + } + )->then(function () use (&$results) { + ksort($results); + return $results; + }); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/functions.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/functions.php new file mode 100644 index 0000000..c03d39d --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/functions.php @@ -0,0 +1,363 @@ + + * while ($eventLoop->isRunning()) { + * GuzzleHttp\Promise\queue()->run(); + * } + * + * + * @param TaskQueueInterface $assign Optionally specify a new queue instance. + * + * @return TaskQueueInterface + * + * @deprecated queue will be removed in guzzlehttp/promises:2.0. Use Utils::queue instead. + */ +function queue(TaskQueueInterface $assign = null) +{ + return Utils::queue($assign); +} + +/** + * Adds a function to run in the task queue when it is next `run()` and returns + * a promise that is fulfilled or rejected with the result. + * + * @param callable $task Task function to run. + * + * @return PromiseInterface + * + * @deprecated task will be removed in guzzlehttp/promises:2.0. Use Utils::task instead. + */ +function task(callable $task) +{ + return Utils::task($task); +} + +/** + * Creates a promise for a value if the value is not a promise. + * + * @param mixed $value Promise or value. + * + * @return PromiseInterface + * + * @deprecated promise_for will be removed in guzzlehttp/promises:2.0. Use Create::promiseFor instead. + */ +function promise_for($value) +{ + return Create::promiseFor($value); +} + +/** + * Creates a rejected promise for a reason if the reason is not a promise. If + * the provided reason is a promise, then it is returned as-is. + * + * @param mixed $reason Promise or reason. + * + * @return PromiseInterface + * + * @deprecated rejection_for will be removed in guzzlehttp/promises:2.0. Use Create::rejectionFor instead. + */ +function rejection_for($reason) +{ + return Create::rejectionFor($reason); +} + +/** + * Create an exception for a rejected promise value. + * + * @param mixed $reason + * + * @return \Exception|\Throwable + * + * @deprecated exception_for will be removed in guzzlehttp/promises:2.0. Use Create::exceptionFor instead. + */ +function exception_for($reason) +{ + return Create::exceptionFor($reason); +} + +/** + * Returns an iterator for the given value. + * + * @param mixed $value + * + * @return \Iterator + * + * @deprecated iter_for will be removed in guzzlehttp/promises:2.0. Use Create::iterFor instead. + */ +function iter_for($value) +{ + return Create::iterFor($value); +} + +/** + * Synchronously waits on a promise to resolve and returns an inspection state + * array. + * + * Returns a state associative array containing a "state" key mapping to a + * valid promise state. If the state of the promise is "fulfilled", the array + * will contain a "value" key mapping to the fulfilled value of the promise. If + * the promise is rejected, the array will contain a "reason" key mapping to + * the rejection reason of the promise. + * + * @param PromiseInterface $promise Promise or value. + * + * @return array + * + * @deprecated inspect will be removed in guzzlehttp/promises:2.0. Use Utils::inspect instead. + */ +function inspect(PromiseInterface $promise) +{ + return Utils::inspect($promise); +} + +/** + * Waits on all of the provided promises, but does not unwrap rejected promises + * as thrown exception. + * + * Returns an array of inspection state arrays. + * + * @see inspect for the inspection state array format. + * + * @param PromiseInterface[] $promises Traversable of promises to wait upon. + * + * @return array + * + * @deprecated inspect will be removed in guzzlehttp/promises:2.0. Use Utils::inspectAll instead. + */ +function inspect_all($promises) +{ + return Utils::inspectAll($promises); +} + +/** + * Waits on all of the provided promises and returns the fulfilled values. + * + * Returns an array that contains the value of each promise (in the same order + * the promises were provided). An exception is thrown if any of the promises + * are rejected. + * + * @param iterable $promises Iterable of PromiseInterface objects to wait on. + * + * @return array + * + * @throws \Exception on error + * @throws \Throwable on error in PHP >=7 + * + * @deprecated unwrap will be removed in guzzlehttp/promises:2.0. Use Utils::unwrap instead. + */ +function unwrap($promises) +{ + return Utils::unwrap($promises); +} + +/** + * Given an array of promises, return a promise that is fulfilled when all the + * items in the array are fulfilled. + * + * The promise's fulfillment value is an array with fulfillment values at + * respective positions to the original array. If any promise in the array + * rejects, the returned promise is rejected with the rejection reason. + * + * @param mixed $promises Promises or values. + * @param bool $recursive If true, resolves new promises that might have been added to the stack during its own resolution. + * + * @return PromiseInterface + * + * @deprecated all will be removed in guzzlehttp/promises:2.0. Use Utils::all instead. + */ +function all($promises, $recursive = false) +{ + return Utils::all($promises, $recursive); +} + +/** + * Initiate a competitive race between multiple promises or values (values will + * become immediately fulfilled promises). + * + * When count amount of promises have been fulfilled, the returned promise is + * fulfilled with an array that contains the fulfillment values of the winners + * in order of resolution. + * + * This promise is rejected with a {@see AggregateException} if the number of + * fulfilled promises is less than the desired $count. + * + * @param int $count Total number of promises. + * @param mixed $promises Promises or values. + * + * @return PromiseInterface + * + * @deprecated some will be removed in guzzlehttp/promises:2.0. Use Utils::some instead. + */ +function some($count, $promises) +{ + return Utils::some($count, $promises); +} + +/** + * Like some(), with 1 as count. However, if the promise fulfills, the + * fulfillment value is not an array of 1 but the value directly. + * + * @param mixed $promises Promises or values. + * + * @return PromiseInterface + * + * @deprecated any will be removed in guzzlehttp/promises:2.0. Use Utils::any instead. + */ +function any($promises) +{ + return Utils::any($promises); +} + +/** + * Returns a promise that is fulfilled when all of the provided promises have + * been fulfilled or rejected. + * + * The returned promise is fulfilled with an array of inspection state arrays. + * + * @see inspect for the inspection state array format. + * + * @param mixed $promises Promises or values. + * + * @return PromiseInterface + * + * @deprecated settle will be removed in guzzlehttp/promises:2.0. Use Utils::settle instead. + */ +function settle($promises) +{ + return Utils::settle($promises); +} + +/** + * Given an iterator that yields promises or values, returns a promise that is + * fulfilled with a null value when the iterator has been consumed or the + * aggregate promise has been fulfilled or rejected. + * + * $onFulfilled is a function that accepts the fulfilled value, iterator index, + * and the aggregate promise. The callback can invoke any necessary side + * effects and choose to resolve or reject the aggregate if needed. + * + * $onRejected is a function that accepts the rejection reason, iterator index, + * and the aggregate promise. The callback can invoke any necessary side + * effects and choose to resolve or reject the aggregate if needed. + * + * @param mixed $iterable Iterator or array to iterate over. + * @param callable $onFulfilled + * @param callable $onRejected + * + * @return PromiseInterface + * + * @deprecated each will be removed in guzzlehttp/promises:2.0. Use Each::of instead. + */ +function each( + $iterable, + callable $onFulfilled = null, + callable $onRejected = null +) { + return Each::of($iterable, $onFulfilled, $onRejected); +} + +/** + * Like each, but only allows a certain number of outstanding promises at any + * given time. + * + * $concurrency may be an integer or a function that accepts the number of + * pending promises and returns a numeric concurrency limit value to allow for + * dynamic a concurrency size. + * + * @param mixed $iterable + * @param int|callable $concurrency + * @param callable $onFulfilled + * @param callable $onRejected + * + * @return PromiseInterface + * + * @deprecated each_limit will be removed in guzzlehttp/promises:2.0. Use Each::ofLimit instead. + */ +function each_limit( + $iterable, + $concurrency, + callable $onFulfilled = null, + callable $onRejected = null +) { + return Each::ofLimit($iterable, $concurrency, $onFulfilled, $onRejected); +} + +/** + * Like each_limit, but ensures that no promise in the given $iterable argument + * is rejected. If any promise is rejected, then the aggregate promise is + * rejected with the encountered rejection. + * + * @param mixed $iterable + * @param int|callable $concurrency + * @param callable $onFulfilled + * + * @return PromiseInterface + * + * @deprecated each_limit_all will be removed in guzzlehttp/promises:2.0. Use Each::ofLimitAll instead. + */ +function each_limit_all( + $iterable, + $concurrency, + callable $onFulfilled = null +) { + return Each::ofLimitAll($iterable, $concurrency, $onFulfilled); +} + +/** + * Returns true if a promise is fulfilled. + * + * @return bool + * + * @deprecated is_fulfilled will be removed in guzzlehttp/promises:2.0. Use Is::fulfilled instead. + */ +function is_fulfilled(PromiseInterface $promise) +{ + return Is::fulfilled($promise); +} + +/** + * Returns true if a promise is rejected. + * + * @return bool + * + * @deprecated is_rejected will be removed in guzzlehttp/promises:2.0. Use Is::rejected instead. + */ +function is_rejected(PromiseInterface $promise) +{ + return Is::rejected($promise); +} + +/** + * Returns true if a promise is fulfilled or rejected. + * + * @return bool + * + * @deprecated is_settled will be removed in guzzlehttp/promises:2.0. Use Is::settled instead. + */ +function is_settled(PromiseInterface $promise) +{ + return Is::settled($promise); +} + +/** + * Create a new coroutine. + * + * @see Coroutine + * + * @return PromiseInterface + * + * @deprecated coroutine will be removed in guzzlehttp/promises:2.0. Use Coroutine::of instead. + */ +function coroutine(callable $generatorFn) +{ + return Coroutine::of($generatorFn); +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/functions_include.php b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/functions_include.php new file mode 100644 index 0000000..34cd171 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/promises/src/functions_include.php @@ -0,0 +1,6 @@ +withPath('foo')->withHost('example.com')` will throw an exception + because the path of a URI with an authority must start with a slash "/" or be empty + - `(new Uri())->withScheme('http')` will return `'http://localhost'` + +### Deprecated + +- `Uri::resolve` in favor of `UriResolver::resolve` +- `Uri::removeDotSegments` in favor of `UriResolver::removeDotSegments` + +### Fixed + +- `Stream::read` when length parameter <= 0. +- `copy_to_stream` reads bytes in chunks instead of `maxLen` into memory. +- `ServerRequest::getUriFromGlobals` when `Host` header contains port. +- Compatibility of URIs with `file` scheme and empty host. + + +## [1.3.1] - 2016-06-25 + +### Fixed + +- `Uri::__toString` for network path references, e.g. `//example.org`. +- Missing lowercase normalization for host. +- Handling of URI components in case they are `'0'` in a lot of places, + e.g. as a user info password. +- `Uri::withAddedHeader` to correctly merge headers with different case. +- Trimming of header values in `Uri::withAddedHeader`. Header values may + be surrounded by whitespace which should be ignored according to RFC 7230 + Section 3.2.4. This does not apply to header names. +- `Uri::withAddedHeader` with an array of header values. +- `Uri::resolve` when base path has no slash and handling of fragment. +- Handling of encoding in `Uri::with(out)QueryValue` so one can pass the + key/value both in encoded as well as decoded form to those methods. This is + consistent with withPath, withQuery etc. +- `ServerRequest::withoutAttribute` when attribute value is null. + + +## [1.3.0] - 2016-04-13 + +### Added + +- Remaining interfaces needed for full PSR7 compatibility + (ServerRequestInterface, UploadedFileInterface, etc.). +- Support for stream_for from scalars. + +### Changed + +- Can now extend Uri. + +### Fixed +- A bug in validating request methods by making it more permissive. + + +## [1.2.3] - 2016-02-18 + +### Fixed + +- Support in `GuzzleHttp\Psr7\CachingStream` for seeking forward on remote + streams, which can sometimes return fewer bytes than requested with `fread`. +- Handling of gzipped responses with FNAME headers. + + +## [1.2.2] - 2016-01-22 + +### Added + +- Support for URIs without any authority. +- Support for HTTP 451 'Unavailable For Legal Reasons.' +- Support for using '0' as a filename. +- Support for including non-standard ports in Host headers. + + +## [1.2.1] - 2015-11-02 + +### Changes + +- Now supporting negative offsets when seeking to SEEK_END. + + +## [1.2.0] - 2015-08-15 + +### Changed + +- Body as `"0"` is now properly added to a response. +- Now allowing forward seeking in CachingStream. +- Now properly parsing HTTP requests that contain proxy targets in + `parse_request`. +- functions.php is now conditionally required. +- user-info is no longer dropped when resolving URIs. + + +## [1.1.0] - 2015-06-24 + +### Changed + +- URIs can now be relative. +- `multipart/form-data` headers are now overridden case-insensitively. +- URI paths no longer encode the following characters because they are allowed + in URIs: "(", ")", "*", "!", "'" +- A port is no longer added to a URI when the scheme is missing and no port is + present. + + +## 1.0.0 - 2015-05-19 + +Initial release. + +Currently unsupported: + +- `Psr\Http\Message\ServerRequestInterface` +- `Psr\Http\Message\UploadedFileInterface` + + + +[1.6.0]: https://github.com/guzzle/psr7/compare/1.5.2...1.6.0 +[1.5.2]: https://github.com/guzzle/psr7/compare/1.5.1...1.5.2 +[1.5.1]: https://github.com/guzzle/psr7/compare/1.5.0...1.5.1 +[1.5.0]: https://github.com/guzzle/psr7/compare/1.4.2...1.5.0 +[1.4.2]: https://github.com/guzzle/psr7/compare/1.4.1...1.4.2 +[1.4.1]: https://github.com/guzzle/psr7/compare/1.4.0...1.4.1 +[1.4.0]: https://github.com/guzzle/psr7/compare/1.3.1...1.4.0 +[1.3.1]: https://github.com/guzzle/psr7/compare/1.3.0...1.3.1 +[1.3.0]: https://github.com/guzzle/psr7/compare/1.2.3...1.3.0 +[1.2.3]: https://github.com/guzzle/psr7/compare/1.2.2...1.2.3 +[1.2.2]: https://github.com/guzzle/psr7/compare/1.2.1...1.2.2 +[1.2.1]: https://github.com/guzzle/psr7/compare/1.2.0...1.2.1 +[1.2.0]: https://github.com/guzzle/psr7/compare/1.1.0...1.2.0 +[1.1.0]: https://github.com/guzzle/psr7/compare/1.0.0...1.1.0 diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/LICENSE b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/LICENSE new file mode 100644 index 0000000..581d95f --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Michael Dowling, https://github.com/mtdowling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/README.md b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/README.md new file mode 100644 index 0000000..acfabfd --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/README.md @@ -0,0 +1,809 @@ +# PSR-7 Message Implementation + +This repository contains a full [PSR-7](http://www.php-fig.org/psr/psr-7/) +message implementation, several stream decorators, and some helpful +functionality like query string parsing. + + +[![Build Status](https://travis-ci.org/guzzle/psr7.svg?branch=master)](https://travis-ci.org/guzzle/psr7) + + +# Stream implementation + +This package comes with a number of stream implementations and stream +decorators. + + +## AppendStream + +`GuzzleHttp\Psr7\AppendStream` + +Reads from multiple streams, one after the other. + +```php +use GuzzleHttp\Psr7; + +$a = Psr7\Utils::streamFor('abc, '); +$b = Psr7\Utils::streamFor('123.'); +$composed = new Psr7\AppendStream([$a, $b]); + +$composed->addStream(Psr7\Utils::streamFor(' Above all listen to me')); + +echo $composed; // abc, 123. Above all listen to me. +``` + + +## BufferStream + +`GuzzleHttp\Psr7\BufferStream` + +Provides a buffer stream that can be written to fill a buffer, and read +from to remove bytes from the buffer. + +This stream returns a "hwm" metadata value that tells upstream consumers +what the configured high water mark of the stream is, or the maximum +preferred size of the buffer. + +```php +use GuzzleHttp\Psr7; + +// When more than 1024 bytes are in the buffer, it will begin returning +// false to writes. This is an indication that writers should slow down. +$buffer = new Psr7\BufferStream(1024); +``` + + +## CachingStream + +The CachingStream is used to allow seeking over previously read bytes on +non-seekable streams. This can be useful when transferring a non-seekable +entity body fails due to needing to rewind the stream (for example, resulting +from a redirect). Data that is read from the remote stream will be buffered in +a PHP temp stream so that previously read bytes are cached first in memory, +then on disk. + +```php +use GuzzleHttp\Psr7; + +$original = Psr7\Utils::streamFor(fopen('http://www.google.com', 'r')); +$stream = new Psr7\CachingStream($original); + +$stream->read(1024); +echo $stream->tell(); +// 1024 + +$stream->seek(0); +echo $stream->tell(); +// 0 +``` + + +## DroppingStream + +`GuzzleHttp\Psr7\DroppingStream` + +Stream decorator that begins dropping data once the size of the underlying +stream becomes too full. + +```php +use GuzzleHttp\Psr7; + +// Create an empty stream +$stream = Psr7\Utils::streamFor(); + +// Start dropping data when the stream has more than 10 bytes +$dropping = new Psr7\DroppingStream($stream, 10); + +$dropping->write('01234567890123456789'); +echo $stream; // 0123456789 +``` + + +## FnStream + +`GuzzleHttp\Psr7\FnStream` + +Compose stream implementations based on a hash of functions. + +Allows for easy testing and extension of a provided stream without needing +to create a concrete class for a simple extension point. + +```php + +use GuzzleHttp\Psr7; + +$stream = Psr7\Utils::streamFor('hi'); +$fnStream = Psr7\FnStream::decorate($stream, [ + 'rewind' => function () use ($stream) { + echo 'About to rewind - '; + $stream->rewind(); + echo 'rewound!'; + } +]); + +$fnStream->rewind(); +// Outputs: About to rewind - rewound! +``` + + +## InflateStream + +`GuzzleHttp\Psr7\InflateStream` + +Uses PHP's zlib.inflate filter to inflate deflate or gzipped content. + +This stream decorator skips the first 10 bytes of the given stream to remove +the gzip header, converts the provided stream to a PHP stream resource, +then appends the zlib.inflate filter. The stream is then converted back +to a Guzzle stream resource to be used as a Guzzle stream. + + +## LazyOpenStream + +`GuzzleHttp\Psr7\LazyOpenStream` + +Lazily reads or writes to a file that is opened only after an IO operation +take place on the stream. + +```php +use GuzzleHttp\Psr7; + +$stream = new Psr7\LazyOpenStream('/path/to/file', 'r'); +// The file has not yet been opened... + +echo $stream->read(10); +// The file is opened and read from only when needed. +``` + + +## LimitStream + +`GuzzleHttp\Psr7\LimitStream` + +LimitStream can be used to read a subset or slice of an existing stream object. +This can be useful for breaking a large file into smaller pieces to be sent in +chunks (e.g. Amazon S3's multipart upload API). + +```php +use GuzzleHttp\Psr7; + +$original = Psr7\Utils::streamFor(fopen('/tmp/test.txt', 'r+')); +echo $original->getSize(); +// >>> 1048576 + +// Limit the size of the body to 1024 bytes and start reading from byte 2048 +$stream = new Psr7\LimitStream($original, 1024, 2048); +echo $stream->getSize(); +// >>> 1024 +echo $stream->tell(); +// >>> 0 +``` + + +## MultipartStream + +`GuzzleHttp\Psr7\MultipartStream` + +Stream that when read returns bytes for a streaming multipart or +multipart/form-data stream. + + +## NoSeekStream + +`GuzzleHttp\Psr7\NoSeekStream` + +NoSeekStream wraps a stream and does not allow seeking. + +```php +use GuzzleHttp\Psr7; + +$original = Psr7\Utils::streamFor('foo'); +$noSeek = new Psr7\NoSeekStream($original); + +echo $noSeek->read(3); +// foo +var_export($noSeek->isSeekable()); +// false +$noSeek->seek(0); +var_export($noSeek->read(3)); +// NULL +``` + + +## PumpStream + +`GuzzleHttp\Psr7\PumpStream` + +Provides a read only stream that pumps data from a PHP callable. + +When invoking the provided callable, the PumpStream will pass the amount of +data requested to read to the callable. The callable can choose to ignore +this value and return fewer or more bytes than requested. Any extra data +returned by the provided callable is buffered internally until drained using +the read() function of the PumpStream. The provided callable MUST return +false when there is no more data to read. + + +## Implementing stream decorators + +Creating a stream decorator is very easy thanks to the +`GuzzleHttp\Psr7\StreamDecoratorTrait`. This trait provides methods that +implement `Psr\Http\Message\StreamInterface` by proxying to an underlying +stream. Just `use` the `StreamDecoratorTrait` and implement your custom +methods. + +For example, let's say we wanted to call a specific function each time the last +byte is read from a stream. This could be implemented by overriding the +`read()` method. + +```php +use Psr\Http\Message\StreamInterface; +use GuzzleHttp\Psr7\StreamDecoratorTrait; + +class EofCallbackStream implements StreamInterface +{ + use StreamDecoratorTrait; + + private $callback; + + public function __construct(StreamInterface $stream, callable $cb) + { + $this->stream = $stream; + $this->callback = $cb; + } + + public function read($length) + { + $result = $this->stream->read($length); + + // Invoke the callback when EOF is hit. + if ($this->eof()) { + call_user_func($this->callback); + } + + return $result; + } +} +``` + +This decorator could be added to any existing stream and used like so: + +```php +use GuzzleHttp\Psr7; + +$original = Psr7\Utils::streamFor('foo'); + +$eofStream = new EofCallbackStream($original, function () { + echo 'EOF!'; +}); + +$eofStream->read(2); +$eofStream->read(1); +// echoes "EOF!" +$eofStream->seek(0); +$eofStream->read(3); +// echoes "EOF!" +``` + + +## PHP StreamWrapper + +You can use the `GuzzleHttp\Psr7\StreamWrapper` class if you need to use a +PSR-7 stream as a PHP stream resource. + +Use the `GuzzleHttp\Psr7\StreamWrapper::getResource()` method to create a PHP +stream from a PSR-7 stream. + +```php +use GuzzleHttp\Psr7\StreamWrapper; + +$stream = GuzzleHttp\Psr7\Utils::streamFor('hello!'); +$resource = StreamWrapper::getResource($stream); +echo fread($resource, 6); // outputs hello! +``` + + +# Static API + +There are various static methods available under the `GuzzleHttp\Psr7` namespace. + + +## `GuzzleHttp\Psr7\Message::toString` + +`public static function toString(MessageInterface $message): string` + +Returns the string representation of an HTTP message. + +```php +$request = new GuzzleHttp\Psr7\Request('GET', 'http://example.com'); +echo GuzzleHttp\Psr7\Message::toString($request); +``` + + +## `GuzzleHttp\Psr7\Message::bodySummary` + +`public static function bodySummary(MessageInterface $message, int $truncateAt = 120): string|null` + +Get a short summary of the message body. + +Will return `null` if the response is not printable. + + +## `GuzzleHttp\Psr7\Message::rewindBody` + +`public static function rewindBody(MessageInterface $message): void` + +Attempts to rewind a message body and throws an exception on failure. + +The body of the message will only be rewound if a call to `tell()` +returns a value other than `0`. + + +## `GuzzleHttp\Psr7\Message::parseMessage` + +`public static function parseMessage(string $message): array` + +Parses an HTTP message into an associative array. + +The array contains the "start-line" key containing the start line of +the message, "headers" key containing an associative array of header +array values, and a "body" key containing the body of the message. + + +## `GuzzleHttp\Psr7\Message::parseRequestUri` + +`public static function parseRequestUri(string $path, array $headers): string` + +Constructs a URI for an HTTP request message. + + +## `GuzzleHttp\Psr7\Message::parseRequest` + +`public static function parseRequest(string $message): Request` + +Parses a request message string into a request object. + + +## `GuzzleHttp\Psr7\Message::parseResponse` + +`public static function parseResponse(string $message): Response` + +Parses a response message string into a response object. + + +## `GuzzleHttp\Psr7\Header::parse` + +`public static function parse(string|array $header): array` + +Parse an array of header values containing ";" separated data into an +array of associative arrays representing the header key value pair data +of the header. When a parameter does not contain a value, but just +contains a key, this function will inject a key with a '' string value. + + +## `GuzzleHttp\Psr7\Header::normalize` + +`public static function normalize(string|array $header): array` + +Converts an array of header values that may contain comma separated +headers into an array of headers with no comma separated values. + + +## `GuzzleHttp\Psr7\Query::parse` + +`public static function parse(string $str, int|bool $urlEncoding = true): array` + +Parse a query string into an associative array. + +If multiple values are found for the same key, the value of that key +value pair will become an array. This function does not parse nested +PHP style arrays into an associative array (e.g., `foo[a]=1&foo[b]=2` +will be parsed into `['foo[a]' => '1', 'foo[b]' => '2'])`. + + +## `GuzzleHttp\Psr7\Query::build` + +`public static function build(array $params, int|false $encoding = PHP_QUERY_RFC3986): string` + +Build a query string from an array of key value pairs. + +This function can use the return value of `parse()` to build a query +string. This function does not modify the provided keys when an array is +encountered (like `http_build_query()` would). + + +## `GuzzleHttp\Psr7\Utils::caselessRemove` + +`public static function caselessRemove(iterable $keys, $keys, array $data): array` + +Remove the items given by the keys, case insensitively from the data. + + +## `GuzzleHttp\Psr7\Utils::copyToStream` + +`public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): void` + +Copy the contents of a stream into another stream until the given number +of bytes have been read. + + +## `GuzzleHttp\Psr7\Utils::copyToString` + +`public static function copyToString(StreamInterface $stream, int $maxLen = -1): string` + +Copy the contents of a stream into a string until the given number of +bytes have been read. + + +## `GuzzleHttp\Psr7\Utils::hash` + +`public static function hash(StreamInterface $stream, string $algo, bool $rawOutput = false): string` + +Calculate a hash of a stream. + +This method reads the entire stream to calculate a rolling hash, based on +PHP's `hash_init` functions. + + +## `GuzzleHttp\Psr7\Utils::modifyRequest` + +`public static function modifyRequest(RequestInterface $request, array $changes): RequestInterface` + +Clone and modify a request with the given changes. + +This method is useful for reducing the number of clones needed to mutate +a message. + +- method: (string) Changes the HTTP method. +- set_headers: (array) Sets the given headers. +- remove_headers: (array) Remove the given headers. +- body: (mixed) Sets the given body. +- uri: (UriInterface) Set the URI. +- query: (string) Set the query string value of the URI. +- version: (string) Set the protocol version. + + +## `GuzzleHttp\Psr7\Utils::readLine` + +`public static function readLine(StreamInterface $stream, int $maxLength = null): string` + +Read a line from the stream up to the maximum allowed buffer length. + + +## `GuzzleHttp\Psr7\Utils::streamFor` + +`public static function streamFor(resource|string|null|int|float|bool|StreamInterface|callable|\Iterator $resource = '', array $options = []): StreamInterface` + +Create a new stream based on the input type. + +Options is an associative array that can contain the following keys: + +- metadata: Array of custom metadata. +- size: Size of the stream. + +This method accepts the following `$resource` types: + +- `Psr\Http\Message\StreamInterface`: Returns the value as-is. +- `string`: Creates a stream object that uses the given string as the contents. +- `resource`: Creates a stream object that wraps the given PHP stream resource. +- `Iterator`: If the provided value implements `Iterator`, then a read-only + stream object will be created that wraps the given iterable. Each time the + stream is read from, data from the iterator will fill a buffer and will be + continuously called until the buffer is equal to the requested read size. + Subsequent read calls will first read from the buffer and then call `next` + on the underlying iterator until it is exhausted. +- `object` with `__toString()`: If the object has the `__toString()` method, + the object will be cast to a string and then a stream will be returned that + uses the string value. +- `NULL`: When `null` is passed, an empty stream object is returned. +- `callable` When a callable is passed, a read-only stream object will be + created that invokes the given callable. The callable is invoked with the + number of suggested bytes to read. The callable can return any number of + bytes, but MUST return `false` when there is no more data to return. The + stream object that wraps the callable will invoke the callable until the + number of requested bytes are available. Any additional bytes will be + buffered and used in subsequent reads. + +```php +$stream = GuzzleHttp\Psr7\Utils::streamFor('foo'); +$stream = GuzzleHttp\Psr7\Utils::streamFor(fopen('/path/to/file', 'r')); + +$generator = function ($bytes) { + for ($i = 0; $i < $bytes; $i++) { + yield ' '; + } +} + +$stream = GuzzleHttp\Psr7\Utils::streamFor($generator(100)); +``` + + +## `GuzzleHttp\Psr7\Utils::tryFopen` + +`public static function tryFopen(string $filename, string $mode): resource` + +Safely opens a PHP stream resource using a filename. + +When fopen fails, PHP normally raises a warning. This function adds an +error handler that checks for errors and throws an exception instead. + + +## `GuzzleHttp\Psr7\Utils::uriFor` + +`public static function uriFor(string|UriInterface $uri): UriInterface` + +Returns a UriInterface for the given value. + +This function accepts a string or UriInterface and returns a +UriInterface for the given value. If the value is already a +UriInterface, it is returned as-is. + + +## `GuzzleHttp\Psr7\MimeType::fromFilename` + +`public static function fromFilename(string $filename): string|null` + +Determines the mimetype of a file by looking at its extension. + + +## `GuzzleHttp\Psr7\MimeType::fromExtension` + +`public static function fromExtension(string $extension): string|null` + +Maps a file extensions to a mimetype. + + +## Upgrading from Function API + +The static API was first introduced in 1.7.0, in order to mitigate problems with functions conflicting between global and local copies of the package. The function API will be removed in 2.0.0. A migration table has been provided here for your convenience: + +| Original Function | Replacement Method | +|----------------|----------------| +| `str` | `Message::toString` | +| `uri_for` | `Utils::uriFor` | +| `stream_for` | `Utils::streamFor` | +| `parse_header` | `Header::parse` | +| `normalize_header` | `Header::normalize` | +| `modify_request` | `Utils::modifyRequest` | +| `rewind_body` | `Message::rewindBody` | +| `try_fopen` | `Utils::tryFopen` | +| `copy_to_string` | `Utils::copyToString` | +| `copy_to_stream` | `Utils::copyToStream` | +| `hash` | `Utils::hash` | +| `readline` | `Utils::readLine` | +| `parse_request` | `Message::parseRequest` | +| `parse_response` | `Message::parseResponse` | +| `parse_query` | `Query::parse` | +| `build_query` | `Query::build` | +| `mimetype_from_filename` | `MimeType::fromFilename` | +| `mimetype_from_extension` | `MimeType::fromExtension` | +| `_parse_message` | `Message::parseMessage` | +| `_parse_request_uri` | `Message::parseRequestUri` | +| `get_message_body_summary` | `Message::bodySummary` | +| `_caseless_remove` | `Utils::caselessRemove` | + + +# Additional URI Methods + +Aside from the standard `Psr\Http\Message\UriInterface` implementation in form of the `GuzzleHttp\Psr7\Uri` class, +this library also provides additional functionality when working with URIs as static methods. + +## URI Types + +An instance of `Psr\Http\Message\UriInterface` can either be an absolute URI or a relative reference. +An absolute URI has a scheme. A relative reference is used to express a URI relative to another URI, +the base URI. Relative references can be divided into several forms according to +[RFC 3986 Section 4.2](https://tools.ietf.org/html/rfc3986#section-4.2): + +- network-path references, e.g. `//example.com/path` +- absolute-path references, e.g. `/path` +- relative-path references, e.g. `subpath` + +The following methods can be used to identify the type of the URI. + +### `GuzzleHttp\Psr7\Uri::isAbsolute` + +`public static function isAbsolute(UriInterface $uri): bool` + +Whether the URI is absolute, i.e. it has a scheme. + +### `GuzzleHttp\Psr7\Uri::isNetworkPathReference` + +`public static function isNetworkPathReference(UriInterface $uri): bool` + +Whether the URI is a network-path reference. A relative reference that begins with two slash characters is +termed an network-path reference. + +### `GuzzleHttp\Psr7\Uri::isAbsolutePathReference` + +`public static function isAbsolutePathReference(UriInterface $uri): bool` + +Whether the URI is a absolute-path reference. A relative reference that begins with a single slash character is +termed an absolute-path reference. + +### `GuzzleHttp\Psr7\Uri::isRelativePathReference` + +`public static function isRelativePathReference(UriInterface $uri): bool` + +Whether the URI is a relative-path reference. A relative reference that does not begin with a slash character is +termed a relative-path reference. + +### `GuzzleHttp\Psr7\Uri::isSameDocumentReference` + +`public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null): bool` + +Whether the URI is a same-document reference. A same-document reference refers to a URI that is, aside from its +fragment component, identical to the base URI. When no base URI is given, only an empty URI reference +(apart from its fragment) is considered a same-document reference. + +## URI Components + +Additional methods to work with URI components. + +### `GuzzleHttp\Psr7\Uri::isDefaultPort` + +`public static function isDefaultPort(UriInterface $uri): bool` + +Whether the URI has the default port of the current scheme. `Psr\Http\Message\UriInterface::getPort` may return null +or the standard port. This method can be used independently of the implementation. + +### `GuzzleHttp\Psr7\Uri::composeComponents` + +`public static function composeComponents($scheme, $authority, $path, $query, $fragment): string` + +Composes a URI reference string from its various components according to +[RFC 3986 Section 5.3](https://tools.ietf.org/html/rfc3986#section-5.3). Usually this method does not need to be called +manually but instead is used indirectly via `Psr\Http\Message\UriInterface::__toString`. + +### `GuzzleHttp\Psr7\Uri::fromParts` + +`public static function fromParts(array $parts): UriInterface` + +Creates a URI from a hash of [`parse_url`](http://php.net/manual/en/function.parse-url.php) components. + + +### `GuzzleHttp\Psr7\Uri::withQueryValue` + +`public static function withQueryValue(UriInterface $uri, $key, $value): UriInterface` + +Creates a new URI with a specific query string value. Any existing query string values that exactly match the +provided key are removed and replaced with the given key value pair. A value of null will set the query string +key without a value, e.g. "key" instead of "key=value". + +### `GuzzleHttp\Psr7\Uri::withQueryValues` + +`public static function withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface` + +Creates a new URI with multiple query string values. It has the same behavior as `withQueryValue()` but for an +associative array of key => value. + +### `GuzzleHttp\Psr7\Uri::withoutQueryValue` + +`public static function withoutQueryValue(UriInterface $uri, $key): UriInterface` + +Creates a new URI with a specific query string value removed. Any existing query string values that exactly match the +provided key are removed. + +## Reference Resolution + +`GuzzleHttp\Psr7\UriResolver` provides methods to resolve a URI reference in the context of a base URI according +to [RFC 3986 Section 5](https://tools.ietf.org/html/rfc3986#section-5). This is for example also what web browsers +do when resolving a link in a website based on the current request URI. + +### `GuzzleHttp\Psr7\UriResolver::resolve` + +`public static function resolve(UriInterface $base, UriInterface $rel): UriInterface` + +Converts the relative URI into a new URI that is resolved against the base URI. + +### `GuzzleHttp\Psr7\UriResolver::removeDotSegments` + +`public static function removeDotSegments(string $path): string` + +Removes dot segments from a path and returns the new path according to +[RFC 3986 Section 5.2.4](https://tools.ietf.org/html/rfc3986#section-5.2.4). + +### `GuzzleHttp\Psr7\UriResolver::relativize` + +`public static function relativize(UriInterface $base, UriInterface $target): UriInterface` + +Returns the target URI as a relative reference from the base URI. This method is the counterpart to resolve(): + +```php +(string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) +``` + +One use-case is to use the current request URI as base URI and then generate relative links in your documents +to reduce the document size or offer self-contained downloadable document archives. + +```php +$base = new Uri('http://example.com/a/b/'); +echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'. +echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'. +echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'. +echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'. +``` + +## Normalization and Comparison + +`GuzzleHttp\Psr7\UriNormalizer` provides methods to normalize and compare URIs according to +[RFC 3986 Section 6](https://tools.ietf.org/html/rfc3986#section-6). + +### `GuzzleHttp\Psr7\UriNormalizer::normalize` + +`public static function normalize(UriInterface $uri, $flags = self::PRESERVING_NORMALIZATIONS): UriInterface` + +Returns a normalized URI. The scheme and host component are already normalized to lowercase per PSR-7 UriInterface. +This methods adds additional normalizations that can be configured with the `$flags` parameter which is a bitmask +of normalizations to apply. The following normalizations are available: + +- `UriNormalizer::PRESERVING_NORMALIZATIONS` + + Default normalizations which only include the ones that preserve semantics. + +- `UriNormalizer::CAPITALIZE_PERCENT_ENCODING` + + All letters within a percent-encoding triplet (e.g., "%3A") are case-insensitive, and should be capitalized. + + Example: `http://example.org/a%c2%b1b` → `http://example.org/a%C2%B1b` + +- `UriNormalizer::DECODE_UNRESERVED_CHARACTERS` + + Decodes percent-encoded octets of unreserved characters. For consistency, percent-encoded octets in the ranges of + ALPHA (%41–%5A and %61–%7A), DIGIT (%30–%39), hyphen (%2D), period (%2E), underscore (%5F), or tilde (%7E) should + not be created by URI producers and, when found in a URI, should be decoded to their corresponding unreserved + characters by URI normalizers. + + Example: `http://example.org/%7Eusern%61me/` → `http://example.org/~username/` + +- `UriNormalizer::CONVERT_EMPTY_PATH` + + Converts the empty path to "/" for http and https URIs. + + Example: `http://example.org` → `http://example.org/` + +- `UriNormalizer::REMOVE_DEFAULT_HOST` + + Removes the default host of the given URI scheme from the URI. Only the "file" scheme defines the default host + "localhost". All of `file:/myfile`, `file:///myfile`, and `file://localhost/myfile` are equivalent according to + RFC 3986. + + Example: `file://localhost/myfile` → `file:///myfile` + +- `UriNormalizer::REMOVE_DEFAULT_PORT` + + Removes the default port of the given URI scheme from the URI. + + Example: `http://example.org:80/` → `http://example.org/` + +- `UriNormalizer::REMOVE_DOT_SEGMENTS` + + Removes unnecessary dot-segments. Dot-segments in relative-path references are not removed as it would + change the semantics of the URI reference. + + Example: `http://example.org/../a/b/../c/./d.html` → `http://example.org/a/c/d.html` + +- `UriNormalizer::REMOVE_DUPLICATE_SLASHES` + + Paths which include two or more adjacent slashes are converted to one. Webservers usually ignore duplicate slashes + and treat those URIs equivalent. But in theory those URIs do not need to be equivalent. So this normalization + may change the semantics. Encoded slashes (%2F) are not removed. + + Example: `http://example.org//foo///bar.html` → `http://example.org/foo/bar.html` + +- `UriNormalizer::SORT_QUERY_PARAMETERS` + + Sort query parameters with their values in alphabetical order. However, the order of parameters in a URI may be + significant (this is not defined by the standard). So this normalization is not safe and may change the semantics + of the URI. + + Example: `?lang=en&article=fred` → `?article=fred&lang=en` + +### `GuzzleHttp\Psr7\UriNormalizer::isEquivalent` + +`public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, $normalizations = self::PRESERVING_NORMALIZATIONS): bool` + +Whether two URIs can be considered equivalent. Both URIs are normalized automatically before comparison with the given +`$normalizations` bitmask. The method also accepts relative URI references and returns true when they are equivalent. +This of course assumes they will be resolved against the same base URI. If this is not the case, determination of +equivalence or difference of relative references does not mean anything. diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/composer.json b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/composer.json new file mode 100644 index 0000000..58dcb07 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/composer.json @@ -0,0 +1,49 @@ +{ + "name": "guzzlehttp/psr7", + "type": "library", + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": ["request", "response", "message", "stream", "http", "uri", "url", "psr-7"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10", + "ext-zlib": "*" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": ["src/functions_include.php"] + }, + "autoload-dev": { + "psr-4": { + "GuzzleHttp\\Tests\\Psr7\\": "tests/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/AppendStream.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/AppendStream.php new file mode 100644 index 0000000..fa9153d --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/AppendStream.php @@ -0,0 +1,246 @@ +addStream($stream); + } + } + + public function __toString() + { + try { + $this->rewind(); + return $this->getContents(); + } catch (\Exception $e) { + return ''; + } + } + + /** + * Add a stream to the AppendStream + * + * @param StreamInterface $stream Stream to append. Must be readable. + * + * @throws \InvalidArgumentException if the stream is not readable + */ + public function addStream(StreamInterface $stream) + { + if (!$stream->isReadable()) { + throw new \InvalidArgumentException('Each stream must be readable'); + } + + // The stream is only seekable if all streams are seekable + if (!$stream->isSeekable()) { + $this->seekable = false; + } + + $this->streams[] = $stream; + } + + public function getContents() + { + return Utils::copyToString($this); + } + + /** + * Closes each attached stream. + * + * {@inheritdoc} + */ + public function close() + { + $this->pos = $this->current = 0; + $this->seekable = true; + + foreach ($this->streams as $stream) { + $stream->close(); + } + + $this->streams = []; + } + + /** + * Detaches each attached stream. + * + * Returns null as it's not clear which underlying stream resource to return. + * + * {@inheritdoc} + */ + public function detach() + { + $this->pos = $this->current = 0; + $this->seekable = true; + + foreach ($this->streams as $stream) { + $stream->detach(); + } + + $this->streams = []; + + return null; + } + + public function tell() + { + return $this->pos; + } + + /** + * Tries to calculate the size by adding the size of each stream. + * + * If any of the streams do not return a valid number, then the size of the + * append stream cannot be determined and null is returned. + * + * {@inheritdoc} + */ + public function getSize() + { + $size = 0; + + foreach ($this->streams as $stream) { + $s = $stream->getSize(); + if ($s === null) { + return null; + } + $size += $s; + } + + return $size; + } + + public function eof() + { + return !$this->streams || + ($this->current >= count($this->streams) - 1 && + $this->streams[$this->current]->eof()); + } + + public function rewind() + { + $this->seek(0); + } + + /** + * Attempts to seek to the given position. Only supports SEEK_SET. + * + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + if (!$this->seekable) { + throw new \RuntimeException('This AppendStream is not seekable'); + } elseif ($whence !== SEEK_SET) { + throw new \RuntimeException('The AppendStream can only seek with SEEK_SET'); + } + + $this->pos = $this->current = 0; + + // Rewind each stream + foreach ($this->streams as $i => $stream) { + try { + $stream->rewind(); + } catch (\Exception $e) { + throw new \RuntimeException('Unable to seek stream ' + . $i . ' of the AppendStream', 0, $e); + } + } + + // Seek to the actual position by reading from each stream + while ($this->pos < $offset && !$this->eof()) { + $result = $this->read(min(8096, $offset - $this->pos)); + if ($result === '') { + break; + } + } + } + + /** + * Reads from all of the appended streams until the length is met or EOF. + * + * {@inheritdoc} + */ + public function read($length) + { + $buffer = ''; + $total = count($this->streams) - 1; + $remaining = $length; + $progressToNext = false; + + while ($remaining > 0) { + + // Progress to the next stream if needed. + if ($progressToNext || $this->streams[$this->current]->eof()) { + $progressToNext = false; + if ($this->current === $total) { + break; + } + $this->current++; + } + + $result = $this->streams[$this->current]->read($remaining); + + // Using a loose comparison here to match on '', false, and null + if ($result == null) { + $progressToNext = true; + continue; + } + + $buffer .= $result; + $remaining = $length - strlen($buffer); + } + + $this->pos += strlen($buffer); + + return $buffer; + } + + public function isReadable() + { + return true; + } + + public function isWritable() + { + return false; + } + + public function isSeekable() + { + return $this->seekable; + } + + public function write($string) + { + throw new \RuntimeException('Cannot write to an AppendStream'); + } + + public function getMetadata($key = null) + { + return $key ? null : []; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/BufferStream.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/BufferStream.php new file mode 100644 index 0000000..783859c --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/BufferStream.php @@ -0,0 +1,142 @@ +hwm = $hwm; + } + + public function __toString() + { + return $this->getContents(); + } + + public function getContents() + { + $buffer = $this->buffer; + $this->buffer = ''; + + return $buffer; + } + + public function close() + { + $this->buffer = ''; + } + + public function detach() + { + $this->close(); + + return null; + } + + public function getSize() + { + return strlen($this->buffer); + } + + public function isReadable() + { + return true; + } + + public function isWritable() + { + return true; + } + + public function isSeekable() + { + return false; + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + throw new \RuntimeException('Cannot seek a BufferStream'); + } + + public function eof() + { + return strlen($this->buffer) === 0; + } + + public function tell() + { + throw new \RuntimeException('Cannot determine the position of a BufferStream'); + } + + /** + * Reads data from the buffer. + */ + public function read($length) + { + $currentLength = strlen($this->buffer); + + if ($length >= $currentLength) { + // No need to slice the buffer because we don't have enough data. + $result = $this->buffer; + $this->buffer = ''; + } else { + // Slice up the result to provide a subset of the buffer. + $result = substr($this->buffer, 0, $length); + $this->buffer = substr($this->buffer, $length); + } + + return $result; + } + + /** + * Writes data to the buffer. + */ + public function write($string) + { + $this->buffer .= $string; + + // TODO: What should happen here? + if (strlen($this->buffer) >= $this->hwm) { + return false; + } + + return strlen($string); + } + + public function getMetadata($key = null) + { + if ($key == 'hwm') { + return $this->hwm; + } + + return $key ? null : []; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/CachingStream.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/CachingStream.php new file mode 100644 index 0000000..fe749e9 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/CachingStream.php @@ -0,0 +1,141 @@ +remoteStream = $stream; + $this->stream = $target ?: new Stream(Utils::tryFopen('php://temp', 'r+')); + } + + public function getSize() + { + return max($this->stream->getSize(), $this->remoteStream->getSize()); + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + if ($whence == SEEK_SET) { + $byte = $offset; + } elseif ($whence == SEEK_CUR) { + $byte = $offset + $this->tell(); + } elseif ($whence == SEEK_END) { + $size = $this->remoteStream->getSize(); + if ($size === null) { + $size = $this->cacheEntireStream(); + } + $byte = $size + $offset; + } else { + throw new \InvalidArgumentException('Invalid whence'); + } + + $diff = $byte - $this->stream->getSize(); + + if ($diff > 0) { + // Read the remoteStream until we have read in at least the amount + // of bytes requested, or we reach the end of the file. + while ($diff > 0 && !$this->remoteStream->eof()) { + $this->read($diff); + $diff = $byte - $this->stream->getSize(); + } + } else { + // We can just do a normal seek since we've already seen this byte. + $this->stream->seek($byte); + } + } + + public function read($length) + { + // Perform a regular read on any previously read data from the buffer + $data = $this->stream->read($length); + $remaining = $length - strlen($data); + + // More data was requested so read from the remote stream + if ($remaining) { + // If data was written to the buffer in a position that would have + // been filled from the remote stream, then we must skip bytes on + // the remote stream to emulate overwriting bytes from that + // position. This mimics the behavior of other PHP stream wrappers. + $remoteData = $this->remoteStream->read( + $remaining + $this->skipReadBytes + ); + + if ($this->skipReadBytes) { + $len = strlen($remoteData); + $remoteData = substr($remoteData, $this->skipReadBytes); + $this->skipReadBytes = max(0, $this->skipReadBytes - $len); + } + + $data .= $remoteData; + $this->stream->write($remoteData); + } + + return $data; + } + + public function write($string) + { + // When appending to the end of the currently read stream, you'll want + // to skip bytes from being read from the remote stream to emulate + // other stream wrappers. Basically replacing bytes of data of a fixed + // length. + $overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell(); + if ($overflow > 0) { + $this->skipReadBytes += $overflow; + } + + return $this->stream->write($string); + } + + public function eof() + { + return $this->stream->eof() && $this->remoteStream->eof(); + } + + /** + * Close both the remote stream and buffer stream + */ + public function close() + { + $this->remoteStream->close() && $this->stream->close(); + } + + private function cacheEntireStream() + { + $target = new FnStream(['write' => 'strlen']); + Utils::copyToStream($this, $target); + + return $this->tell(); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/DroppingStream.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/DroppingStream.php new file mode 100644 index 0000000..9f7420c --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/DroppingStream.php @@ -0,0 +1,45 @@ +stream = $stream; + $this->maxLength = $maxLength; + } + + public function write($string) + { + $diff = $this->maxLength - $this->stream->getSize(); + + // Begin returning 0 when the underlying stream is too large. + if ($diff <= 0) { + return 0; + } + + // Write the stream or a subset of the stream if needed. + if (strlen($string) < $diff) { + return $this->stream->write($string); + } + + return $this->stream->write(substr($string, 0, $diff)); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/FnStream.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/FnStream.php new file mode 100644 index 0000000..76a8cc7 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/FnStream.php @@ -0,0 +1,163 @@ +methods = $methods; + + // Create the functions on the class + foreach ($methods as $name => $fn) { + $this->{'_fn_' . $name} = $fn; + } + } + + /** + * Lazily determine which methods are not implemented. + * + * @throws \BadMethodCallException + */ + public function __get($name) + { + throw new \BadMethodCallException(str_replace('_fn_', '', $name) + . '() is not implemented in the FnStream'); + } + + /** + * The close method is called on the underlying stream only if possible. + */ + public function __destruct() + { + if (isset($this->_fn_close)) { + call_user_func($this->_fn_close); + } + } + + /** + * An unserialize would allow the __destruct to run when the unserialized value goes out of scope. + * + * @throws \LogicException + */ + public function __wakeup() + { + throw new \LogicException('FnStream should never be unserialized'); + } + + /** + * Adds custom functionality to an underlying stream by intercepting + * specific method calls. + * + * @param StreamInterface $stream Stream to decorate + * @param array $methods Hash of method name to a closure + * + * @return FnStream + */ + public static function decorate(StreamInterface $stream, array $methods) + { + // If any of the required methods were not provided, then simply + // proxy to the decorated stream. + foreach (array_diff(self::$slots, array_keys($methods)) as $diff) { + $methods[$diff] = [$stream, $diff]; + } + + return new self($methods); + } + + public function __toString() + { + return call_user_func($this->_fn___toString); + } + + public function close() + { + return call_user_func($this->_fn_close); + } + + public function detach() + { + return call_user_func($this->_fn_detach); + } + + public function getSize() + { + return call_user_func($this->_fn_getSize); + } + + public function tell() + { + return call_user_func($this->_fn_tell); + } + + public function eof() + { + return call_user_func($this->_fn_eof); + } + + public function isSeekable() + { + return call_user_func($this->_fn_isSeekable); + } + + public function rewind() + { + call_user_func($this->_fn_rewind); + } + + public function seek($offset, $whence = SEEK_SET) + { + call_user_func($this->_fn_seek, $offset, $whence); + } + + public function isWritable() + { + return call_user_func($this->_fn_isWritable); + } + + public function write($string) + { + return call_user_func($this->_fn_write, $string); + } + + public function isReadable() + { + return call_user_func($this->_fn_isReadable); + } + + public function read($length) + { + return call_user_func($this->_fn_read, $length); + } + + public function getContents() + { + return call_user_func($this->_fn_getContents); + } + + public function getMetadata($key = null) + { + return call_user_func($this->_fn_getMetadata, $key); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Header.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Header.php new file mode 100644 index 0000000..865d742 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Header.php @@ -0,0 +1,71 @@ +]+>|[^=]+/', $kvp, $matches)) { + $m = $matches[0]; + if (isset($m[1])) { + $part[trim($m[0], $trimmed)] = trim($m[1], $trimmed); + } else { + $part[] = trim($m[0], $trimmed); + } + } + } + if ($part) { + $params[] = $part; + } + } + + return $params; + } + + /** + * Converts an array of header values that may contain comma separated + * headers into an array of headers with no comma separated values. + * + * @param string|array $header Header to normalize. + * + * @return array Returns the normalized header field values. + */ + public static function normalize($header) + { + if (!is_array($header)) { + return array_map('trim', explode(',', $header)); + } + + $result = []; + foreach ($header as $value) { + foreach ((array) $value as $v) { + if (strpos($v, ',') === false) { + $result[] = $v; + continue; + } + foreach (preg_split('/,(?=([^"]*"[^"]*")*[^"]*$)/', $v) as $vv) { + $result[] = trim($vv); + } + } + } + + return $result; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/InflateStream.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/InflateStream.php new file mode 100644 index 0000000..0cbd2cc --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/InflateStream.php @@ -0,0 +1,56 @@ +read(10); + $filenameHeaderLength = $this->getLengthOfPossibleFilenameHeader($stream, $header); + // Skip the header, that is 10 + length of filename + 1 (nil) bytes + $stream = new LimitStream($stream, -1, 10 + $filenameHeaderLength); + $resource = StreamWrapper::getResource($stream); + stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ); + $this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource)); + } + + /** + * @param StreamInterface $stream + * @param $header + * + * @return int + */ + private function getLengthOfPossibleFilenameHeader(StreamInterface $stream, $header) + { + $filename_header_length = 0; + + if (substr(bin2hex($header), 6, 2) === '08') { + // we have a filename, read until nil + $filename_header_length = 1; + while ($stream->read(1) !== chr(0)) { + $filename_header_length++; + } + } + + return $filename_header_length; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/LazyOpenStream.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/LazyOpenStream.php new file mode 100644 index 0000000..911e127 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/LazyOpenStream.php @@ -0,0 +1,42 @@ +filename = $filename; + $this->mode = $mode; + } + + /** + * Creates the underlying stream lazily when required. + * + * @return StreamInterface + */ + protected function createStream() + { + return Utils::streamFor(Utils::tryFopen($this->filename, $this->mode)); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/LimitStream.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/LimitStream.php new file mode 100644 index 0000000..1173ec4 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/LimitStream.php @@ -0,0 +1,157 @@ +stream = $stream; + $this->setLimit($limit); + $this->setOffset($offset); + } + + public function eof() + { + // Always return true if the underlying stream is EOF + if ($this->stream->eof()) { + return true; + } + + // No limit and the underlying stream is not at EOF + if ($this->limit == -1) { + return false; + } + + return $this->stream->tell() >= $this->offset + $this->limit; + } + + /** + * Returns the size of the limited subset of data + * {@inheritdoc} + */ + public function getSize() + { + if (null === ($length = $this->stream->getSize())) { + return null; + } elseif ($this->limit == -1) { + return $length - $this->offset; + } else { + return min($this->limit, $length - $this->offset); + } + } + + /** + * Allow for a bounded seek on the read limited stream + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + if ($whence !== SEEK_SET || $offset < 0) { + throw new \RuntimeException(sprintf( + 'Cannot seek to offset %s with whence %s', + $offset, + $whence + )); + } + + $offset += $this->offset; + + if ($this->limit !== -1) { + if ($offset > $this->offset + $this->limit) { + $offset = $this->offset + $this->limit; + } + } + + $this->stream->seek($offset); + } + + /** + * Give a relative tell() + * {@inheritdoc} + */ + public function tell() + { + return $this->stream->tell() - $this->offset; + } + + /** + * Set the offset to start limiting from + * + * @param int $offset Offset to seek to and begin byte limiting from + * + * @throws \RuntimeException if the stream cannot be seeked. + */ + public function setOffset($offset) + { + $current = $this->stream->tell(); + + if ($current !== $offset) { + // If the stream cannot seek to the offset position, then read to it + if ($this->stream->isSeekable()) { + $this->stream->seek($offset); + } elseif ($current > $offset) { + throw new \RuntimeException("Could not seek to stream offset $offset"); + } else { + $this->stream->read($offset - $current); + } + } + + $this->offset = $offset; + } + + /** + * Set the limit of bytes that the decorator allows to be read from the + * stream. + * + * @param int $limit Number of bytes to allow to be read from the stream. + * Use -1 for no limit. + */ + public function setLimit($limit) + { + $this->limit = $limit; + } + + public function read($length) + { + if ($this->limit == -1) { + return $this->stream->read($length); + } + + // Check if the current position is less than the total allowed + // bytes + original offset + $remaining = ($this->offset + $this->limit) - $this->stream->tell(); + if ($remaining > 0) { + // Only return the amount of requested data, ensuring that the byte + // limit is not exceeded + return $this->stream->read(min($remaining, $length)); + } + + return ''; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Message.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Message.php new file mode 100644 index 0000000..516d1cb --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Message.php @@ -0,0 +1,252 @@ +getMethod() . ' ' + . $message->getRequestTarget()) + . ' HTTP/' . $message->getProtocolVersion(); + if (!$message->hasHeader('host')) { + $msg .= "\r\nHost: " . $message->getUri()->getHost(); + } + } elseif ($message instanceof ResponseInterface) { + $msg = 'HTTP/' . $message->getProtocolVersion() . ' ' + . $message->getStatusCode() . ' ' + . $message->getReasonPhrase(); + } else { + throw new \InvalidArgumentException('Unknown message type'); + } + + foreach ($message->getHeaders() as $name => $values) { + if (strtolower($name) === 'set-cookie') { + foreach ($values as $value) { + $msg .= "\r\n{$name}: " . $value; + } + } else { + $msg .= "\r\n{$name}: " . implode(', ', $values); + } + } + + return "{$msg}\r\n\r\n" . $message->getBody(); + } + + /** + * Get a short summary of the message body. + * + * Will return `null` if the response is not printable. + * + * @param MessageInterface $message The message to get the body summary + * @param int $truncateAt The maximum allowed size of the summary + * + * @return string|null + */ + public static function bodySummary(MessageInterface $message, $truncateAt = 120) + { + $body = $message->getBody(); + + if (!$body->isSeekable() || !$body->isReadable()) { + return null; + } + + $size = $body->getSize(); + + if ($size === 0) { + return null; + } + + $summary = $body->read($truncateAt); + $body->rewind(); + + if ($size > $truncateAt) { + $summary .= ' (truncated...)'; + } + + // Matches any printable character, including unicode characters: + // letters, marks, numbers, punctuation, spacing, and separators. + if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary)) { + return null; + } + + return $summary; + } + + /** + * Attempts to rewind a message body and throws an exception on failure. + * + * The body of the message will only be rewound if a call to `tell()` + * returns a value other than `0`. + * + * @param MessageInterface $message Message to rewind + * + * @throws \RuntimeException + */ + public static function rewindBody(MessageInterface $message) + { + $body = $message->getBody(); + + if ($body->tell()) { + $body->rewind(); + } + } + + /** + * Parses an HTTP message into an associative array. + * + * The array contains the "start-line" key containing the start line of + * the message, "headers" key containing an associative array of header + * array values, and a "body" key containing the body of the message. + * + * @param string $message HTTP request or response to parse. + * + * @return array + */ + public static function parseMessage($message) + { + if (!$message) { + throw new \InvalidArgumentException('Invalid message'); + } + + $message = ltrim($message, "\r\n"); + + $messageParts = preg_split("/\r?\n\r?\n/", $message, 2); + + if ($messageParts === false || count($messageParts) !== 2) { + throw new \InvalidArgumentException('Invalid message: Missing header delimiter'); + } + + list($rawHeaders, $body) = $messageParts; + $rawHeaders .= "\r\n"; // Put back the delimiter we split previously + $headerParts = preg_split("/\r?\n/", $rawHeaders, 2); + + if ($headerParts === false || count($headerParts) !== 2) { + throw new \InvalidArgumentException('Invalid message: Missing status line'); + } + + list($startLine, $rawHeaders) = $headerParts; + + if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') { + // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0 + $rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders); + } + + /** @var array[] $headerLines */ + $count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER); + + // If these aren't the same, then one line didn't match and there's an invalid header. + if ($count !== substr_count($rawHeaders, "\n")) { + // Folding is deprecated, see https://tools.ietf.org/html/rfc7230#section-3.2.4 + if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) { + throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding'); + } + + throw new \InvalidArgumentException('Invalid header syntax'); + } + + $headers = []; + + foreach ($headerLines as $headerLine) { + $headers[$headerLine[1]][] = $headerLine[2]; + } + + return [ + 'start-line' => $startLine, + 'headers' => $headers, + 'body' => $body, + ]; + } + + /** + * Constructs a URI for an HTTP request message. + * + * @param string $path Path from the start-line + * @param array $headers Array of headers (each value an array). + * + * @return string + */ + public static function parseRequestUri($path, array $headers) + { + $hostKey = array_filter(array_keys($headers), function ($k) { + return strtolower($k) === 'host'; + }); + + // If no host is found, then a full URI cannot be constructed. + if (!$hostKey) { + return $path; + } + + $host = $headers[reset($hostKey)][0]; + $scheme = substr($host, -4) === ':443' ? 'https' : 'http'; + + return $scheme . '://' . $host . '/' . ltrim($path, '/'); + } + + /** + * Parses a request message string into a request object. + * + * @param string $message Request message string. + * + * @return Request + */ + public static function parseRequest($message) + { + $data = self::parseMessage($message); + $matches = []; + if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) { + throw new \InvalidArgumentException('Invalid request string'); + } + $parts = explode(' ', $data['start-line'], 3); + $version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1'; + + $request = new Request( + $parts[0], + $matches[1] === '/' ? self::parseRequestUri($parts[1], $data['headers']) : $parts[1], + $data['headers'], + $data['body'], + $version + ); + + return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]); + } + + /** + * Parses a response message string into a response object. + * + * @param string $message Response message string. + * + * @return Response + */ + public static function parseResponse($message) + { + $data = self::parseMessage($message); + // According to https://tools.ietf.org/html/rfc7230#section-3.1.2 the space + // between status-code and reason-phrase is required. But browsers accept + // responses without space and reason as well. + if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) { + throw new \InvalidArgumentException('Invalid response string: ' . $data['start-line']); + } + $parts = explode(' ', $data['start-line'], 3); + + return new Response( + (int) $parts[1], + $data['headers'], + $data['body'], + explode('/', $parts[0])[1], + isset($parts[2]) ? $parts[2] : null + ); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/MessageTrait.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/MessageTrait.php new file mode 100644 index 0000000..99203bb --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/MessageTrait.php @@ -0,0 +1,214 @@ + array of values */ + private $headers = []; + + /** @var array Map of lowercase header name => original name at registration */ + private $headerNames = []; + + /** @var string */ + private $protocol = '1.1'; + + /** @var StreamInterface|null */ + private $stream; + + public function getProtocolVersion() + { + return $this->protocol; + } + + public function withProtocolVersion($version) + { + if ($this->protocol === $version) { + return $this; + } + + $new = clone $this; + $new->protocol = $version; + return $new; + } + + public function getHeaders() + { + return $this->headers; + } + + public function hasHeader($header) + { + return isset($this->headerNames[strtolower($header)]); + } + + public function getHeader($header) + { + $header = strtolower($header); + + if (!isset($this->headerNames[$header])) { + return []; + } + + $header = $this->headerNames[$header]; + + return $this->headers[$header]; + } + + public function getHeaderLine($header) + { + return implode(', ', $this->getHeader($header)); + } + + public function withHeader($header, $value) + { + $this->assertHeader($header); + $value = $this->normalizeHeaderValue($value); + $normalized = strtolower($header); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + unset($new->headers[$new->headerNames[$normalized]]); + } + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + + return $new; + } + + public function withAddedHeader($header, $value) + { + $this->assertHeader($header); + $value = $this->normalizeHeaderValue($value); + $normalized = strtolower($header); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $new->headers[$header] = array_merge($this->headers[$header], $value); + } else { + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + } + + return $new; + } + + public function withoutHeader($header) + { + $normalized = strtolower($header); + + if (!isset($this->headerNames[$normalized])) { + return $this; + } + + $header = $this->headerNames[$normalized]; + + $new = clone $this; + unset($new->headers[$header], $new->headerNames[$normalized]); + + return $new; + } + + public function getBody() + { + if (!$this->stream) { + $this->stream = Utils::streamFor(''); + } + + return $this->stream; + } + + public function withBody(StreamInterface $body) + { + if ($body === $this->stream) { + return $this; + } + + $new = clone $this; + $new->stream = $body; + return $new; + } + + private function setHeaders(array $headers) + { + $this->headerNames = $this->headers = []; + foreach ($headers as $header => $value) { + if (is_int($header)) { + // Numeric array keys are converted to int by PHP but having a header name '123' is not forbidden by the spec + // and also allowed in withHeader(). So we need to cast it to string again for the following assertion to pass. + $header = (string) $header; + } + $this->assertHeader($header); + $value = $this->normalizeHeaderValue($value); + $normalized = strtolower($header); + if (isset($this->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $this->headers[$header] = array_merge($this->headers[$header], $value); + } else { + $this->headerNames[$normalized] = $header; + $this->headers[$header] = $value; + } + } + } + + private function normalizeHeaderValue($value) + { + if (!is_array($value)) { + return $this->trimHeaderValues([$value]); + } + + if (count($value) === 0) { + throw new \InvalidArgumentException('Header value can not be an empty array.'); + } + + return $this->trimHeaderValues($value); + } + + /** + * Trims whitespace from the header values. + * + * Spaces and tabs ought to be excluded by parsers when extracting the field value from a header field. + * + * header-field = field-name ":" OWS field-value OWS + * OWS = *( SP / HTAB ) + * + * @param string[] $values Header values + * + * @return string[] Trimmed header values + * + * @see https://tools.ietf.org/html/rfc7230#section-3.2.4 + */ + private function trimHeaderValues(array $values) + { + return array_map(function ($value) { + if (!is_scalar($value) && null !== $value) { + throw new \InvalidArgumentException(sprintf( + 'Header value must be scalar or null but %s provided.', + is_object($value) ? get_class($value) : gettype($value) + )); + } + + return trim((string) $value, " \t"); + }, array_values($values)); + } + + private function assertHeader($header) + { + if (!is_string($header)) { + throw new \InvalidArgumentException(sprintf( + 'Header name must be a string but %s provided.', + is_object($header) ? get_class($header) : gettype($header) + )); + } + + if ($header === '') { + throw new \InvalidArgumentException('Header name can not be empty.'); + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/MimeType.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/MimeType.php new file mode 100644 index 0000000..205c7b1 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/MimeType.php @@ -0,0 +1,140 @@ + 'video/3gpp', + '7z' => 'application/x-7z-compressed', + 'aac' => 'audio/x-aac', + 'ai' => 'application/postscript', + 'aif' => 'audio/x-aiff', + 'asc' => 'text/plain', + 'asf' => 'video/x-ms-asf', + 'atom' => 'application/atom+xml', + 'avi' => 'video/x-msvideo', + 'bmp' => 'image/bmp', + 'bz2' => 'application/x-bzip2', + 'cer' => 'application/pkix-cert', + 'crl' => 'application/pkix-crl', + 'crt' => 'application/x-x509-ca-cert', + 'css' => 'text/css', + 'csv' => 'text/csv', + 'cu' => 'application/cu-seeme', + 'deb' => 'application/x-debian-package', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dvi' => 'application/x-dvi', + 'eot' => 'application/vnd.ms-fontobject', + 'eps' => 'application/postscript', + 'epub' => 'application/epub+zip', + 'etx' => 'text/x-setext', + 'flac' => 'audio/flac', + 'flv' => 'video/x-flv', + 'gif' => 'image/gif', + 'gz' => 'application/gzip', + 'htm' => 'text/html', + 'html' => 'text/html', + 'ico' => 'image/x-icon', + 'ics' => 'text/calendar', + 'ini' => 'text/plain', + 'iso' => 'application/x-iso9660-image', + 'jar' => 'application/java-archive', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'js' => 'text/javascript', + 'json' => 'application/json', + 'latex' => 'application/x-latex', + 'log' => 'text/plain', + 'm4a' => 'audio/mp4', + 'm4v' => 'video/mp4', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mov' => 'video/quicktime', + 'mkv' => 'video/x-matroska', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mp4a' => 'audio/mp4', + 'mp4v' => 'video/mp4', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpg4' => 'video/mp4', + 'oga' => 'audio/ogg', + 'ogg' => 'audio/ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'pbm' => 'image/x-portable-bitmap', + 'pdf' => 'application/pdf', + 'pgm' => 'image/x-portable-graymap', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'ppm' => 'image/x-portable-pixmap', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ps' => 'application/postscript', + 'qt' => 'video/quicktime', + 'rar' => 'application/x-rar-compressed', + 'ras' => 'image/x-cmu-raster', + 'rss' => 'application/rss+xml', + 'rtf' => 'application/rtf', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'svg' => 'image/svg+xml', + 'swf' => 'application/x-shockwave-flash', + 'tar' => 'application/x-tar', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'torrent' => 'application/x-bittorrent', + 'ttf' => 'application/x-font-ttf', + 'txt' => 'text/plain', + 'wav' => 'audio/x-wav', + 'webm' => 'video/webm', + 'webp' => 'image/webp', + 'wma' => 'audio/x-ms-wma', + 'wmv' => 'video/x-ms-wmv', + 'woff' => 'application/x-font-woff', + 'wsdl' => 'application/wsdl+xml', + 'xbm' => 'image/x-xbitmap', + 'xls' => 'application/vnd.ms-excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xml' => 'application/xml', + 'xpm' => 'image/x-xpixmap', + 'xwd' => 'image/x-xwindowdump', + 'yaml' => 'text/yaml', + 'yml' => 'text/yaml', + 'zip' => 'application/zip', + ]; + + $extension = strtolower($extension); + + return isset($mimetypes[$extension]) + ? $mimetypes[$extension] + : null; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/MultipartStream.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/MultipartStream.php new file mode 100644 index 0000000..5a6079a --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/MultipartStream.php @@ -0,0 +1,158 @@ +boundary = $boundary ?: sha1(uniqid('', true)); + $this->stream = $this->createStream($elements); + } + + /** + * Get the boundary + * + * @return string + */ + public function getBoundary() + { + return $this->boundary; + } + + public function isWritable() + { + return false; + } + + /** + * Get the headers needed before transferring the content of a POST file + */ + private function getHeaders(array $headers) + { + $str = ''; + foreach ($headers as $key => $value) { + $str .= "{$key}: {$value}\r\n"; + } + + return "--{$this->boundary}\r\n" . trim($str) . "\r\n\r\n"; + } + + /** + * Create the aggregate stream that will be used to upload the POST data + */ + protected function createStream(array $elements) + { + $stream = new AppendStream(); + + foreach ($elements as $element) { + $this->addElement($stream, $element); + } + + // Add the trailing boundary with CRLF + $stream->addStream(Utils::streamFor("--{$this->boundary}--\r\n")); + + return $stream; + } + + private function addElement(AppendStream $stream, array $element) + { + foreach (['contents', 'name'] as $key) { + if (!array_key_exists($key, $element)) { + throw new \InvalidArgumentException("A '{$key}' key is required"); + } + } + + $element['contents'] = Utils::streamFor($element['contents']); + + if (empty($element['filename'])) { + $uri = $element['contents']->getMetadata('uri'); + if (substr($uri, 0, 6) !== 'php://') { + $element['filename'] = $uri; + } + } + + list($body, $headers) = $this->createElement( + $element['name'], + $element['contents'], + isset($element['filename']) ? $element['filename'] : null, + isset($element['headers']) ? $element['headers'] : [] + ); + + $stream->addStream(Utils::streamFor($this->getHeaders($headers))); + $stream->addStream($body); + $stream->addStream(Utils::streamFor("\r\n")); + } + + /** + * @return array + */ + private function createElement($name, StreamInterface $stream, $filename, array $headers) + { + // Set a default content-disposition header if one was no provided + $disposition = $this->getHeader($headers, 'content-disposition'); + if (!$disposition) { + $headers['Content-Disposition'] = ($filename === '0' || $filename) + ? sprintf( + 'form-data; name="%s"; filename="%s"', + $name, + basename($filename) + ) + : "form-data; name=\"{$name}\""; + } + + // Set a default content-length header if one was no provided + $length = $this->getHeader($headers, 'content-length'); + if (!$length) { + if ($length = $stream->getSize()) { + $headers['Content-Length'] = (string) $length; + } + } + + // Set a default Content-Type if one was not supplied + $type = $this->getHeader($headers, 'content-type'); + if (!$type && ($filename === '0' || $filename)) { + if ($type = MimeType::fromFilename($filename)) { + $headers['Content-Type'] = $type; + } + } + + return [$stream, $headers]; + } + + private function getHeader(array $headers, $key) + { + $lowercaseHeader = strtolower($key); + foreach ($headers as $k => $v) { + if (strtolower($k) === $lowercaseHeader) { + return $v; + } + } + + return null; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/NoSeekStream.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/NoSeekStream.php new file mode 100644 index 0000000..d66bdde --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/NoSeekStream.php @@ -0,0 +1,25 @@ +source = $source; + $this->size = isset($options['size']) ? $options['size'] : null; + $this->metadata = isset($options['metadata']) ? $options['metadata'] : []; + $this->buffer = new BufferStream(); + } + + public function __toString() + { + try { + return Utils::copyToString($this); + } catch (\Exception $e) { + return ''; + } + } + + public function close() + { + $this->detach(); + } + + public function detach() + { + $this->tellPos = false; + $this->source = null; + + return null; + } + + public function getSize() + { + return $this->size; + } + + public function tell() + { + return $this->tellPos; + } + + public function eof() + { + return !$this->source; + } + + public function isSeekable() + { + return false; + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + throw new \RuntimeException('Cannot seek a PumpStream'); + } + + public function isWritable() + { + return false; + } + + public function write($string) + { + throw new \RuntimeException('Cannot write to a PumpStream'); + } + + public function isReadable() + { + return true; + } + + public function read($length) + { + $data = $this->buffer->read($length); + $readLen = strlen($data); + $this->tellPos += $readLen; + $remaining = $length - $readLen; + + if ($remaining) { + $this->pump($remaining); + $data .= $this->buffer->read($remaining); + $this->tellPos += strlen($data) - $readLen; + } + + return $data; + } + + public function getContents() + { + $result = ''; + while (!$this->eof()) { + $result .= $this->read(1000000); + } + + return $result; + } + + public function getMetadata($key = null) + { + if (!$key) { + return $this->metadata; + } + + return isset($this->metadata[$key]) ? $this->metadata[$key] : null; + } + + private function pump($length) + { + if ($this->source) { + do { + $data = call_user_func($this->source, $length); + if ($data === false || $data === null) { + $this->source = null; + return; + } + $this->buffer->write($data); + $length -= strlen($data); + } while ($length > 0); + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Query.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Query.php new file mode 100644 index 0000000..5a7cc03 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Query.php @@ -0,0 +1,113 @@ + '1', 'foo[b]' => '2'])`. + * + * @param string $str Query string to parse + * @param int|bool $urlEncoding How the query string is encoded + * + * @return array + */ + public static function parse($str, $urlEncoding = true) + { + $result = []; + + if ($str === '') { + return $result; + } + + if ($urlEncoding === true) { + $decoder = function ($value) { + return rawurldecode(str_replace('+', ' ', $value)); + }; + } elseif ($urlEncoding === PHP_QUERY_RFC3986) { + $decoder = 'rawurldecode'; + } elseif ($urlEncoding === PHP_QUERY_RFC1738) { + $decoder = 'urldecode'; + } else { + $decoder = function ($str) { + return $str; + }; + } + + foreach (explode('&', $str) as $kvp) { + $parts = explode('=', $kvp, 2); + $key = $decoder($parts[0]); + $value = isset($parts[1]) ? $decoder($parts[1]) : null; + if (!isset($result[$key])) { + $result[$key] = $value; + } else { + if (!is_array($result[$key])) { + $result[$key] = [$result[$key]]; + } + $result[$key][] = $value; + } + } + + return $result; + } + + /** + * Build a query string from an array of key value pairs. + * + * This function can use the return value of `parse()` to build a query + * string. This function does not modify the provided keys when an array is + * encountered (like `http_build_query()` would). + * + * @param array $params Query string parameters. + * @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986 + * to encode using RFC3986, or PHP_QUERY_RFC1738 + * to encode using RFC1738. + * + * @return string + */ + public static function build(array $params, $encoding = PHP_QUERY_RFC3986) + { + if (!$params) { + return ''; + } + + if ($encoding === false) { + $encoder = function ($str) { + return $str; + }; + } elseif ($encoding === PHP_QUERY_RFC3986) { + $encoder = 'rawurlencode'; + } elseif ($encoding === PHP_QUERY_RFC1738) { + $encoder = 'urlencode'; + } else { + throw new \InvalidArgumentException('Invalid type'); + } + + $qs = ''; + foreach ($params as $k => $v) { + $k = $encoder($k); + if (!is_array($v)) { + $qs .= $k; + if ($v !== null) { + $qs .= '=' . $encoder($v); + } + $qs .= '&'; + } else { + foreach ($v as $vv) { + $qs .= $k; + if ($vv !== null) { + $qs .= '=' . $encoder($vv); + } + $qs .= '&'; + } + } + } + + return $qs ? (string) substr($qs, 0, -1) : ''; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Request.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Request.php new file mode 100644 index 0000000..c1cdaeb --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Request.php @@ -0,0 +1,152 @@ +assertMethod($method); + if (!($uri instanceof UriInterface)) { + $uri = new Uri($uri); + } + + $this->method = strtoupper($method); + $this->uri = $uri; + $this->setHeaders($headers); + $this->protocol = $version; + + if (!isset($this->headerNames['host'])) { + $this->updateHostFromUri(); + } + + if ($body !== '' && $body !== null) { + $this->stream = Utils::streamFor($body); + } + } + + public function getRequestTarget() + { + if ($this->requestTarget !== null) { + return $this->requestTarget; + } + + $target = $this->uri->getPath(); + if ($target == '') { + $target = '/'; + } + if ($this->uri->getQuery() != '') { + $target .= '?' . $this->uri->getQuery(); + } + + return $target; + } + + public function withRequestTarget($requestTarget) + { + if (preg_match('#\s#', $requestTarget)) { + throw new InvalidArgumentException( + 'Invalid request target provided; cannot contain whitespace' + ); + } + + $new = clone $this; + $new->requestTarget = $requestTarget; + return $new; + } + + public function getMethod() + { + return $this->method; + } + + public function withMethod($method) + { + $this->assertMethod($method); + $new = clone $this; + $new->method = strtoupper($method); + return $new; + } + + public function getUri() + { + return $this->uri; + } + + public function withUri(UriInterface $uri, $preserveHost = false) + { + if ($uri === $this->uri) { + return $this; + } + + $new = clone $this; + $new->uri = $uri; + + if (!$preserveHost || !isset($this->headerNames['host'])) { + $new->updateHostFromUri(); + } + + return $new; + } + + private function updateHostFromUri() + { + $host = $this->uri->getHost(); + + if ($host == '') { + return; + } + + if (($port = $this->uri->getPort()) !== null) { + $host .= ':' . $port; + } + + if (isset($this->headerNames['host'])) { + $header = $this->headerNames['host']; + } else { + $header = 'Host'; + $this->headerNames['host'] = 'Host'; + } + // Ensure Host is the first header. + // See: http://tools.ietf.org/html/rfc7230#section-5.4 + $this->headers = [$header => [$host]] + $this->headers; + } + + private function assertMethod($method) + { + if (!is_string($method) || $method === '') { + throw new \InvalidArgumentException('Method must be a non-empty string.'); + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Response.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Response.php new file mode 100644 index 0000000..8c01a0f --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Response.php @@ -0,0 +1,155 @@ + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-status', + 208 => 'Already Reported', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Switch Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Requested range not satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 511 => 'Network Authentication Required', + ]; + + /** @var string */ + private $reasonPhrase = ''; + + /** @var int */ + private $statusCode = 200; + + /** + * @param int $status Status code + * @param array $headers Response headers + * @param string|resource|StreamInterface|null $body Response body + * @param string $version Protocol version + * @param string|null $reason Reason phrase (when empty a default will be used based on the status code) + */ + public function __construct( + $status = 200, + array $headers = [], + $body = null, + $version = '1.1', + $reason = null + ) { + $this->assertStatusCodeIsInteger($status); + $status = (int) $status; + $this->assertStatusCodeRange($status); + + $this->statusCode = $status; + + if ($body !== '' && $body !== null) { + $this->stream = Utils::streamFor($body); + } + + $this->setHeaders($headers); + if ($reason == '' && isset(self::$phrases[$this->statusCode])) { + $this->reasonPhrase = self::$phrases[$this->statusCode]; + } else { + $this->reasonPhrase = (string) $reason; + } + + $this->protocol = $version; + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function getReasonPhrase() + { + return $this->reasonPhrase; + } + + public function withStatus($code, $reasonPhrase = '') + { + $this->assertStatusCodeIsInteger($code); + $code = (int) $code; + $this->assertStatusCodeRange($code); + + $new = clone $this; + $new->statusCode = $code; + if ($reasonPhrase == '' && isset(self::$phrases[$new->statusCode])) { + $reasonPhrase = self::$phrases[$new->statusCode]; + } + $new->reasonPhrase = (string) $reasonPhrase; + return $new; + } + + private function assertStatusCodeIsInteger($statusCode) + { + if (filter_var($statusCode, FILTER_VALIDATE_INT) === false) { + throw new \InvalidArgumentException('Status code must be an integer value.'); + } + } + + private function assertStatusCodeRange($statusCode) + { + if ($statusCode < 100 || $statusCode >= 600) { + throw new \InvalidArgumentException('Status code must be an integer value between 1xx and 5xx.'); + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Rfc7230.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Rfc7230.php new file mode 100644 index 0000000..51b571f --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Rfc7230.php @@ -0,0 +1,19 @@ +@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m"; + const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)"; +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/ServerRequest.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/ServerRequest.php new file mode 100644 index 0000000..e6d26f5 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/ServerRequest.php @@ -0,0 +1,379 @@ +serverParams = $serverParams; + + parent::__construct($method, $uri, $headers, $body, $version); + } + + /** + * Return an UploadedFile instance array. + * + * @param array $files A array which respect $_FILES structure + * + * @return array + * + * @throws InvalidArgumentException for unrecognized values + */ + public static function normalizeFiles(array $files) + { + $normalized = []; + + foreach ($files as $key => $value) { + if ($value instanceof UploadedFileInterface) { + $normalized[$key] = $value; + } elseif (is_array($value) && isset($value['tmp_name'])) { + $normalized[$key] = self::createUploadedFileFromSpec($value); + } elseif (is_array($value)) { + $normalized[$key] = self::normalizeFiles($value); + continue; + } else { + throw new InvalidArgumentException('Invalid value in files specification'); + } + } + + return $normalized; + } + + /** + * Create and return an UploadedFile instance from a $_FILES specification. + * + * If the specification represents an array of values, this method will + * delegate to normalizeNestedFileSpec() and return that return value. + * + * @param array $value $_FILES struct + * + * @return array|UploadedFileInterface + */ + private static function createUploadedFileFromSpec(array $value) + { + if (is_array($value['tmp_name'])) { + return self::normalizeNestedFileSpec($value); + } + + return new UploadedFile( + $value['tmp_name'], + (int) $value['size'], + (int) $value['error'], + $value['name'], + $value['type'] + ); + } + + /** + * Normalize an array of file specifications. + * + * Loops through all nested files and returns a normalized array of + * UploadedFileInterface instances. + * + * @param array $files + * + * @return UploadedFileInterface[] + */ + private static function normalizeNestedFileSpec(array $files = []) + { + $normalizedFiles = []; + + foreach (array_keys($files['tmp_name']) as $key) { + $spec = [ + 'tmp_name' => $files['tmp_name'][$key], + 'size' => $files['size'][$key], + 'error' => $files['error'][$key], + 'name' => $files['name'][$key], + 'type' => $files['type'][$key], + ]; + $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec); + } + + return $normalizedFiles; + } + + /** + * Return a ServerRequest populated with superglobals: + * $_GET + * $_POST + * $_COOKIE + * $_FILES + * $_SERVER + * + * @return ServerRequestInterface + */ + public static function fromGlobals() + { + $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + $headers = getallheaders(); + $uri = self::getUriFromGlobals(); + $body = new CachingStream(new LazyOpenStream('php://input', 'r+')); + $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1'; + + $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER); + + return $serverRequest + ->withCookieParams($_COOKIE) + ->withQueryParams($_GET) + ->withParsedBody($_POST) + ->withUploadedFiles(self::normalizeFiles($_FILES)); + } + + private static function extractHostAndPortFromAuthority($authority) + { + $uri = 'http://' . $authority; + $parts = parse_url($uri); + if (false === $parts) { + return [null, null]; + } + + $host = isset($parts['host']) ? $parts['host'] : null; + $port = isset($parts['port']) ? $parts['port'] : null; + + return [$host, $port]; + } + + /** + * Get a Uri populated with values from $_SERVER. + * + * @return UriInterface + */ + public static function getUriFromGlobals() + { + $uri = new Uri(''); + + $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http'); + + $hasPort = false; + if (isset($_SERVER['HTTP_HOST'])) { + list($host, $port) = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']); + if ($host !== null) { + $uri = $uri->withHost($host); + } + + if ($port !== null) { + $hasPort = true; + $uri = $uri->withPort($port); + } + } elseif (isset($_SERVER['SERVER_NAME'])) { + $uri = $uri->withHost($_SERVER['SERVER_NAME']); + } elseif (isset($_SERVER['SERVER_ADDR'])) { + $uri = $uri->withHost($_SERVER['SERVER_ADDR']); + } + + if (!$hasPort && isset($_SERVER['SERVER_PORT'])) { + $uri = $uri->withPort($_SERVER['SERVER_PORT']); + } + + $hasQuery = false; + if (isset($_SERVER['REQUEST_URI'])) { + $requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2); + $uri = $uri->withPath($requestUriParts[0]); + if (isset($requestUriParts[1])) { + $hasQuery = true; + $uri = $uri->withQuery($requestUriParts[1]); + } + } + + if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) { + $uri = $uri->withQuery($_SERVER['QUERY_STRING']); + } + + return $uri; + } + + /** + * {@inheritdoc} + */ + public function getServerParams() + { + return $this->serverParams; + } + + /** + * {@inheritdoc} + */ + public function getUploadedFiles() + { + return $this->uploadedFiles; + } + + /** + * {@inheritdoc} + */ + public function withUploadedFiles(array $uploadedFiles) + { + $new = clone $this; + $new->uploadedFiles = $uploadedFiles; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getCookieParams() + { + return $this->cookieParams; + } + + /** + * {@inheritdoc} + */ + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->cookieParams = $cookies; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getQueryParams() + { + return $this->queryParams; + } + + /** + * {@inheritdoc} + */ + public function withQueryParams(array $query) + { + $new = clone $this; + $new->queryParams = $query; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getParsedBody() + { + return $this->parsedBody; + } + + /** + * {@inheritdoc} + */ + public function withParsedBody($data) + { + $new = clone $this; + $new->parsedBody = $data; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getAttributes() + { + return $this->attributes; + } + + /** + * {@inheritdoc} + */ + public function getAttribute($attribute, $default = null) + { + if (false === array_key_exists($attribute, $this->attributes)) { + return $default; + } + + return $this->attributes[$attribute]; + } + + /** + * {@inheritdoc} + */ + public function withAttribute($attribute, $value) + { + $new = clone $this; + $new->attributes[$attribute] = $value; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function withoutAttribute($attribute) + { + if (false === array_key_exists($attribute, $this->attributes)) { + return $this; + } + + $new = clone $this; + unset($new->attributes[$attribute]); + + return $new; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Stream.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Stream.php new file mode 100644 index 0000000..3865d6d --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Stream.php @@ -0,0 +1,270 @@ +size = $options['size']; + } + + $this->customMetadata = isset($options['metadata']) + ? $options['metadata'] + : []; + + $this->stream = $stream; + $meta = stream_get_meta_data($this->stream); + $this->seekable = $meta['seekable']; + $this->readable = (bool)preg_match(self::READABLE_MODES, $meta['mode']); + $this->writable = (bool)preg_match(self::WRITABLE_MODES, $meta['mode']); + $this->uri = $this->getMetadata('uri'); + } + + /** + * Closes the stream when the destructed + */ + public function __destruct() + { + $this->close(); + } + + public function __toString() + { + try { + if ($this->isSeekable()) { + $this->seek(0); + } + return $this->getContents(); + } catch (\Exception $e) { + return ''; + } + } + + public function getContents() + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + $contents = stream_get_contents($this->stream); + + if ($contents === false) { + throw new \RuntimeException('Unable to read stream contents'); + } + + return $contents; + } + + public function close() + { + if (isset($this->stream)) { + if (is_resource($this->stream)) { + fclose($this->stream); + } + $this->detach(); + } + } + + public function detach() + { + if (!isset($this->stream)) { + return null; + } + + $result = $this->stream; + unset($this->stream); + $this->size = $this->uri = null; + $this->readable = $this->writable = $this->seekable = false; + + return $result; + } + + public function getSize() + { + if ($this->size !== null) { + return $this->size; + } + + if (!isset($this->stream)) { + return null; + } + + // Clear the stat cache if the stream has a URI + if ($this->uri) { + clearstatcache(true, $this->uri); + } + + $stats = fstat($this->stream); + if (isset($stats['size'])) { + $this->size = $stats['size']; + return $this->size; + } + + return null; + } + + public function isReadable() + { + return $this->readable; + } + + public function isWritable() + { + return $this->writable; + } + + public function isSeekable() + { + return $this->seekable; + } + + public function eof() + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + return feof($this->stream); + } + + public function tell() + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + $result = ftell($this->stream); + + if ($result === false) { + throw new \RuntimeException('Unable to determine stream position'); + } + + return $result; + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + $whence = (int) $whence; + + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->seekable) { + throw new \RuntimeException('Stream is not seekable'); + } + if (fseek($this->stream, $offset, $whence) === -1) { + throw new \RuntimeException('Unable to seek to stream position ' + . $offset . ' with whence ' . var_export($whence, true)); + } + } + + public function read($length) + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->readable) { + throw new \RuntimeException('Cannot read from non-readable stream'); + } + if ($length < 0) { + throw new \RuntimeException('Length parameter cannot be negative'); + } + + if (0 === $length) { + return ''; + } + + $string = fread($this->stream, $length); + if (false === $string) { + throw new \RuntimeException('Unable to read from stream'); + } + + return $string; + } + + public function write($string) + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->writable) { + throw new \RuntimeException('Cannot write to a non-writable stream'); + } + + // We can't know the size after writing anything + $this->size = null; + $result = fwrite($this->stream, $string); + + if ($result === false) { + throw new \RuntimeException('Unable to write to stream'); + } + + return $result; + } + + public function getMetadata($key = null) + { + if (!isset($this->stream)) { + return $key ? null : []; + } elseif (!$key) { + return $this->customMetadata + stream_get_meta_data($this->stream); + } elseif (isset($this->customMetadata[$key])) { + return $this->customMetadata[$key]; + } + + $meta = stream_get_meta_data($this->stream); + + return isset($meta[$key]) ? $meta[$key] : null; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php new file mode 100644 index 0000000..5025dd6 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php @@ -0,0 +1,152 @@ +stream = $stream; + } + + /** + * Magic method used to create a new stream if streams are not added in + * the constructor of a decorator (e.g., LazyOpenStream). + * + * @param string $name Name of the property (allows "stream" only). + * + * @return StreamInterface + */ + public function __get($name) + { + if ($name == 'stream') { + $this->stream = $this->createStream(); + return $this->stream; + } + + throw new \UnexpectedValueException("$name not found on class"); + } + + public function __toString() + { + try { + if ($this->isSeekable()) { + $this->seek(0); + } + return $this->getContents(); + } catch (\Exception $e) { + // Really, PHP? https://bugs.php.net/bug.php?id=53648 + trigger_error('StreamDecorator::__toString exception: ' + . (string) $e, E_USER_ERROR); + return ''; + } + } + + public function getContents() + { + return Utils::copyToString($this); + } + + /** + * Allow decorators to implement custom methods + * + * @param string $method Missing method name + * @param array $args Method arguments + * + * @return mixed + */ + public function __call($method, array $args) + { + $result = call_user_func_array([$this->stream, $method], $args); + + // Always return the wrapped object if the result is a return $this + return $result === $this->stream ? $this : $result; + } + + public function close() + { + $this->stream->close(); + } + + public function getMetadata($key = null) + { + return $this->stream->getMetadata($key); + } + + public function detach() + { + return $this->stream->detach(); + } + + public function getSize() + { + return $this->stream->getSize(); + } + + public function eof() + { + return $this->stream->eof(); + } + + public function tell() + { + return $this->stream->tell(); + } + + public function isReadable() + { + return $this->stream->isReadable(); + } + + public function isWritable() + { + return $this->stream->isWritable(); + } + + public function isSeekable() + { + return $this->stream->isSeekable(); + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + $this->stream->seek($offset, $whence); + } + + public function read($length) + { + return $this->stream->read($length); + } + + public function write($string) + { + return $this->stream->write($string); + } + + /** + * Implement in subclasses to dynamically create streams when requested. + * + * @return StreamInterface + * + * @throws \BadMethodCallException + */ + protected function createStream() + { + throw new \BadMethodCallException('Not implemented'); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/StreamWrapper.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/StreamWrapper.php new file mode 100644 index 0000000..fc7cb96 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/StreamWrapper.php @@ -0,0 +1,165 @@ +isReadable()) { + $mode = $stream->isWritable() ? 'r+' : 'r'; + } elseif ($stream->isWritable()) { + $mode = 'w'; + } else { + throw new \InvalidArgumentException('The stream must be readable, ' + . 'writable, or both.'); + } + + return fopen('guzzle://stream', $mode, null, self::createStreamContext($stream)); + } + + /** + * Creates a stream context that can be used to open a stream as a php stream resource. + * + * @param StreamInterface $stream + * + * @return resource + */ + public static function createStreamContext(StreamInterface $stream) + { + return stream_context_create([ + 'guzzle' => ['stream' => $stream] + ]); + } + + /** + * Registers the stream wrapper if needed + */ + public static function register() + { + if (!in_array('guzzle', stream_get_wrappers())) { + stream_wrapper_register('guzzle', __CLASS__); + } + } + + public function stream_open($path, $mode, $options, &$opened_path) + { + $options = stream_context_get_options($this->context); + + if (!isset($options['guzzle']['stream'])) { + return false; + } + + $this->mode = $mode; + $this->stream = $options['guzzle']['stream']; + + return true; + } + + public function stream_read($count) + { + return $this->stream->read($count); + } + + public function stream_write($data) + { + return (int) $this->stream->write($data); + } + + public function stream_tell() + { + return $this->stream->tell(); + } + + public function stream_eof() + { + return $this->stream->eof(); + } + + public function stream_seek($offset, $whence) + { + $this->stream->seek($offset, $whence); + + return true; + } + + public function stream_cast($cast_as) + { + $stream = clone($this->stream); + + return $stream->detach(); + } + + public function stream_stat() + { + static $modeMap = [ + 'r' => 33060, + 'rb' => 33060, + 'r+' => 33206, + 'w' => 33188, + 'wb' => 33188 + ]; + + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => $modeMap[$this->mode], + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => $this->stream->getSize() ?: 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0 + ]; + } + + public function url_stat($path, $flags) + { + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0 + ]; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/UploadedFile.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/UploadedFile.php new file mode 100644 index 0000000..bf342c4 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/UploadedFile.php @@ -0,0 +1,328 @@ +setError($errorStatus); + $this->setSize($size); + $this->setClientFilename($clientFilename); + $this->setClientMediaType($clientMediaType); + + if ($this->isOk()) { + $this->setStreamOrFile($streamOrFile); + } + } + + /** + * Depending on the value set file or stream variable + * + * @param mixed $streamOrFile + * + * @throws InvalidArgumentException + */ + private function setStreamOrFile($streamOrFile) + { + if (is_string($streamOrFile)) { + $this->file = $streamOrFile; + } elseif (is_resource($streamOrFile)) { + $this->stream = new Stream($streamOrFile); + } elseif ($streamOrFile instanceof StreamInterface) { + $this->stream = $streamOrFile; + } else { + throw new InvalidArgumentException( + 'Invalid stream or file provided for UploadedFile' + ); + } + } + + /** + * @param int $error + * + * @throws InvalidArgumentException + */ + private function setError($error) + { + if (false === is_int($error)) { + throw new InvalidArgumentException( + 'Upload file error status must be an integer' + ); + } + + if (false === in_array($error, UploadedFile::$errors)) { + throw new InvalidArgumentException( + 'Invalid error status for UploadedFile' + ); + } + + $this->error = $error; + } + + /** + * @param int $size + * + * @throws InvalidArgumentException + */ + private function setSize($size) + { + if (false === is_int($size)) { + throw new InvalidArgumentException( + 'Upload file size must be an integer' + ); + } + + $this->size = $size; + } + + /** + * @param mixed $param + * + * @return bool + */ + private function isStringOrNull($param) + { + return in_array(gettype($param), ['string', 'NULL']); + } + + /** + * @param mixed $param + * + * @return bool + */ + private function isStringNotEmpty($param) + { + return is_string($param) && false === empty($param); + } + + /** + * @param string|null $clientFilename + * + * @throws InvalidArgumentException + */ + private function setClientFilename($clientFilename) + { + if (false === $this->isStringOrNull($clientFilename)) { + throw new InvalidArgumentException( + 'Upload file client filename must be a string or null' + ); + } + + $this->clientFilename = $clientFilename; + } + + /** + * @param string|null $clientMediaType + * + * @throws InvalidArgumentException + */ + private function setClientMediaType($clientMediaType) + { + if (false === $this->isStringOrNull($clientMediaType)) { + throw new InvalidArgumentException( + 'Upload file client media type must be a string or null' + ); + } + + $this->clientMediaType = $clientMediaType; + } + + /** + * Return true if there is no upload error + * + * @return bool + */ + private function isOk() + { + return $this->error === UPLOAD_ERR_OK; + } + + /** + * @return bool + */ + public function isMoved() + { + return $this->moved; + } + + /** + * @throws RuntimeException if is moved or not ok + */ + private function validateActive() + { + if (false === $this->isOk()) { + throw new RuntimeException('Cannot retrieve stream due to upload error'); + } + + if ($this->isMoved()) { + throw new RuntimeException('Cannot retrieve stream after it has already been moved'); + } + } + + /** + * {@inheritdoc} + * + * @throws RuntimeException if the upload was not successful. + */ + public function getStream() + { + $this->validateActive(); + + if ($this->stream instanceof StreamInterface) { + return $this->stream; + } + + return new LazyOpenStream($this->file, 'r+'); + } + + /** + * {@inheritdoc} + * + * @see http://php.net/is_uploaded_file + * @see http://php.net/move_uploaded_file + * + * @param string $targetPath Path to which to move the uploaded file. + * + * @throws RuntimeException if the upload was not successful. + * @throws InvalidArgumentException if the $path specified is invalid. + * @throws RuntimeException on any error during the move operation, or on + * the second or subsequent call to the method. + */ + public function moveTo($targetPath) + { + $this->validateActive(); + + if (false === $this->isStringNotEmpty($targetPath)) { + throw new InvalidArgumentException( + 'Invalid path provided for move operation; must be a non-empty string' + ); + } + + if ($this->file) { + $this->moved = php_sapi_name() == 'cli' + ? rename($this->file, $targetPath) + : move_uploaded_file($this->file, $targetPath); + } else { + Utils::copyToStream( + $this->getStream(), + new LazyOpenStream($targetPath, 'w') + ); + + $this->moved = true; + } + + if (false === $this->moved) { + throw new RuntimeException( + sprintf('Uploaded file could not be moved to %s', $targetPath) + ); + } + } + + /** + * {@inheritdoc} + * + * @return int|null The file size in bytes or null if unknown. + */ + public function getSize() + { + return $this->size; + } + + /** + * {@inheritdoc} + * + * @see http://php.net/manual/en/features.file-upload.errors.php + * + * @return int One of PHP's UPLOAD_ERR_XXX constants. + */ + public function getError() + { + return $this->error; + } + + /** + * {@inheritdoc} + * + * @return string|null The filename sent by the client or null if none + * was provided. + */ + public function getClientFilename() + { + return $this->clientFilename; + } + + /** + * {@inheritdoc} + */ + public function getClientMediaType() + { + return $this->clientMediaType; + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Uri.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Uri.php new file mode 100644 index 0000000..0f9f020 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Uri.php @@ -0,0 +1,810 @@ + 80, + 'https' => 443, + 'ftp' => 21, + 'gopher' => 70, + 'nntp' => 119, + 'news' => 119, + 'telnet' => 23, + 'tn3270' => 23, + 'imap' => 143, + 'pop' => 110, + 'ldap' => 389, + ]; + + private static $charUnreserved = 'a-zA-Z0-9_\-\.~'; + private static $charSubDelims = '!\$&\'\(\)\*\+,;='; + private static $replaceQuery = ['=' => '%3D', '&' => '%26']; + + /** @var string Uri scheme. */ + private $scheme = ''; + + /** @var string Uri user info. */ + private $userInfo = ''; + + /** @var string Uri host. */ + private $host = ''; + + /** @var int|null Uri port. */ + private $port; + + /** @var string Uri path. */ + private $path = ''; + + /** @var string Uri query string. */ + private $query = ''; + + /** @var string Uri fragment. */ + private $fragment = ''; + + /** + * @param string $uri URI to parse + */ + public function __construct($uri = '') + { + // weak type check to also accept null until we can add scalar type hints + if ($uri != '') { + $parts = self::parse($uri); + if ($parts === false) { + throw new \InvalidArgumentException("Unable to parse URI: $uri"); + } + $this->applyParts($parts); + } + } + + /** + * UTF-8 aware \parse_url() replacement. + * + * The internal function produces broken output for non ASCII domain names + * (IDN) when used with locales other than "C". + * + * On the other hand, cURL understands IDN correctly only when UTF-8 locale + * is configured ("C.UTF-8", "en_US.UTF-8", etc.). + * + * @see https://bugs.php.net/bug.php?id=52923 + * @see https://www.php.net/manual/en/function.parse-url.php#114817 + * @see https://curl.haxx.se/libcurl/c/CURLOPT_URL.html#ENCODING + * + * @param string $url + * + * @return array|false + */ + private static function parse($url) + { + // If IPv6 + $prefix = ''; + if (preg_match('%^(.*://\[[0-9:a-f]+\])(.*?)$%', $url, $matches)) { + $prefix = $matches[1]; + $url = $matches[2]; + } + + $encodedUrl = preg_replace_callback( + '%[^:/@?&=#]+%usD', + static function ($matches) { + return urlencode($matches[0]); + }, + $url + ); + + $result = parse_url($prefix . $encodedUrl); + + if ($result === false) { + return false; + } + + return array_map('urldecode', $result); + } + + public function __toString() + { + return self::composeComponents( + $this->scheme, + $this->getAuthority(), + $this->path, + $this->query, + $this->fragment + ); + } + + /** + * Composes a URI reference string from its various components. + * + * Usually this method does not need to be called manually but instead is used indirectly via + * `Psr\Http\Message\UriInterface::__toString`. + * + * PSR-7 UriInterface treats an empty component the same as a missing component as + * getQuery(), getFragment() etc. always return a string. This explains the slight + * difference to RFC 3986 Section 5.3. + * + * Another adjustment is that the authority separator is added even when the authority is missing/empty + * for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with + * `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But + * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to + * that format). + * + * @param string $scheme + * @param string $authority + * @param string $path + * @param string $query + * @param string $fragment + * + * @return string + * + * @link https://tools.ietf.org/html/rfc3986#section-5.3 + */ + public static function composeComponents($scheme, $authority, $path, $query, $fragment) + { + $uri = ''; + + // weak type checks to also accept null until we can add scalar type hints + if ($scheme != '') { + $uri .= $scheme . ':'; + } + + if ($authority != ''|| $scheme === 'file') { + $uri .= '//' . $authority; + } + + $uri .= $path; + + if ($query != '') { + $uri .= '?' . $query; + } + + if ($fragment != '') { + $uri .= '#' . $fragment; + } + + return $uri; + } + + /** + * Whether the URI has the default port of the current scheme. + * + * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used + * independently of the implementation. + * + * @param UriInterface $uri + * + * @return bool + */ + public static function isDefaultPort(UriInterface $uri) + { + return $uri->getPort() === null + || (isset(self::$defaultPorts[$uri->getScheme()]) && $uri->getPort() === self::$defaultPorts[$uri->getScheme()]); + } + + /** + * Whether the URI is absolute, i.e. it has a scheme. + * + * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true + * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative + * to another URI, the base URI. Relative references can be divided into several forms: + * - network-path references, e.g. '//example.com/path' + * - absolute-path references, e.g. '/path' + * - relative-path references, e.g. 'subpath' + * + * @param UriInterface $uri + * + * @return bool + * + * @see Uri::isNetworkPathReference + * @see Uri::isAbsolutePathReference + * @see Uri::isRelativePathReference + * @link https://tools.ietf.org/html/rfc3986#section-4 + */ + public static function isAbsolute(UriInterface $uri) + { + return $uri->getScheme() !== ''; + } + + /** + * Whether the URI is a network-path reference. + * + * A relative reference that begins with two slash characters is termed an network-path reference. + * + * @param UriInterface $uri + * + * @return bool + * + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public static function isNetworkPathReference(UriInterface $uri) + { + return $uri->getScheme() === '' && $uri->getAuthority() !== ''; + } + + /** + * Whether the URI is a absolute-path reference. + * + * A relative reference that begins with a single slash character is termed an absolute-path reference. + * + * @param UriInterface $uri + * + * @return bool + * + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public static function isAbsolutePathReference(UriInterface $uri) + { + return $uri->getScheme() === '' + && $uri->getAuthority() === '' + && isset($uri->getPath()[0]) + && $uri->getPath()[0] === '/'; + } + + /** + * Whether the URI is a relative-path reference. + * + * A relative reference that does not begin with a slash character is termed a relative-path reference. + * + * @param UriInterface $uri + * + * @return bool + * + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public static function isRelativePathReference(UriInterface $uri) + { + return $uri->getScheme() === '' + && $uri->getAuthority() === '' + && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/'); + } + + /** + * Whether the URI is a same-document reference. + * + * A same-document reference refers to a URI that is, aside from its fragment + * component, identical to the base URI. When no base URI is given, only an empty + * URI reference (apart from its fragment) is considered a same-document reference. + * + * @param UriInterface $uri The URI to check + * @param UriInterface|null $base An optional base URI to compare against + * + * @return bool + * + * @link https://tools.ietf.org/html/rfc3986#section-4.4 + */ + public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null) + { + if ($base !== null) { + $uri = UriResolver::resolve($base, $uri); + + return ($uri->getScheme() === $base->getScheme()) + && ($uri->getAuthority() === $base->getAuthority()) + && ($uri->getPath() === $base->getPath()) + && ($uri->getQuery() === $base->getQuery()); + } + + return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === ''; + } + + /** + * Removes dot segments from a path and returns the new path. + * + * @param string $path + * + * @return string + * + * @deprecated since version 1.4. Use UriResolver::removeDotSegments instead. + * @see UriResolver::removeDotSegments + */ + public static function removeDotSegments($path) + { + return UriResolver::removeDotSegments($path); + } + + /** + * Converts the relative URI into a new URI that is resolved against the base URI. + * + * @param UriInterface $base Base URI + * @param string|UriInterface $rel Relative URI + * + * @return UriInterface + * + * @deprecated since version 1.4. Use UriResolver::resolve instead. + * @see UriResolver::resolve + */ + public static function resolve(UriInterface $base, $rel) + { + if (!($rel instanceof UriInterface)) { + $rel = new self($rel); + } + + return UriResolver::resolve($base, $rel); + } + + /** + * Creates a new URI with a specific query string value removed. + * + * Any existing query string values that exactly match the provided key are + * removed. + * + * @param UriInterface $uri URI to use as a base. + * @param string $key Query string key to remove. + * + * @return UriInterface + */ + public static function withoutQueryValue(UriInterface $uri, $key) + { + $result = self::getFilteredQueryString($uri, [$key]); + + return $uri->withQuery(implode('&', $result)); + } + + /** + * Creates a new URI with a specific query string value. + * + * Any existing query string values that exactly match the provided key are + * removed and replaced with the given key value pair. + * + * A value of null will set the query string key without a value, e.g. "key" + * instead of "key=value". + * + * @param UriInterface $uri URI to use as a base. + * @param string $key Key to set. + * @param string|null $value Value to set + * + * @return UriInterface + */ + public static function withQueryValue(UriInterface $uri, $key, $value) + { + $result = self::getFilteredQueryString($uri, [$key]); + + $result[] = self::generateQueryString($key, $value); + + return $uri->withQuery(implode('&', $result)); + } + + /** + * Creates a new URI with multiple specific query string values. + * + * It has the same behavior as withQueryValue() but for an associative array of key => value. + * + * @param UriInterface $uri URI to use as a base. + * @param array $keyValueArray Associative array of key and values + * + * @return UriInterface + */ + public static function withQueryValues(UriInterface $uri, array $keyValueArray) + { + $result = self::getFilteredQueryString($uri, array_keys($keyValueArray)); + + foreach ($keyValueArray as $key => $value) { + $result[] = self::generateQueryString($key, $value); + } + + return $uri->withQuery(implode('&', $result)); + } + + /** + * Creates a URI from a hash of `parse_url` components. + * + * @param array $parts + * + * @return UriInterface + * + * @link http://php.net/manual/en/function.parse-url.php + * + * @throws \InvalidArgumentException If the components do not form a valid URI. + */ + public static function fromParts(array $parts) + { + $uri = new self(); + $uri->applyParts($parts); + $uri->validateState(); + + return $uri; + } + + public function getScheme() + { + return $this->scheme; + } + + public function getAuthority() + { + $authority = $this->host; + if ($this->userInfo !== '') { + $authority = $this->userInfo . '@' . $authority; + } + + if ($this->port !== null) { + $authority .= ':' . $this->port; + } + + return $authority; + } + + public function getUserInfo() + { + return $this->userInfo; + } + + public function getHost() + { + return $this->host; + } + + public function getPort() + { + return $this->port; + } + + public function getPath() + { + return $this->path; + } + + public function getQuery() + { + return $this->query; + } + + public function getFragment() + { + return $this->fragment; + } + + public function withScheme($scheme) + { + $scheme = $this->filterScheme($scheme); + + if ($this->scheme === $scheme) { + return $this; + } + + $new = clone $this; + $new->scheme = $scheme; + $new->removeDefaultPort(); + $new->validateState(); + + return $new; + } + + public function withUserInfo($user, $password = null) + { + $info = $this->filterUserInfoComponent($user); + if ($password !== null) { + $info .= ':' . $this->filterUserInfoComponent($password); + } + + if ($this->userInfo === $info) { + return $this; + } + + $new = clone $this; + $new->userInfo = $info; + $new->validateState(); + + return $new; + } + + public function withHost($host) + { + $host = $this->filterHost($host); + + if ($this->host === $host) { + return $this; + } + + $new = clone $this; + $new->host = $host; + $new->validateState(); + + return $new; + } + + public function withPort($port) + { + $port = $this->filterPort($port); + + if ($this->port === $port) { + return $this; + } + + $new = clone $this; + $new->port = $port; + $new->removeDefaultPort(); + $new->validateState(); + + return $new; + } + + public function withPath($path) + { + $path = $this->filterPath($path); + + if ($this->path === $path) { + return $this; + } + + $new = clone $this; + $new->path = $path; + $new->validateState(); + + return $new; + } + + public function withQuery($query) + { + $query = $this->filterQueryAndFragment($query); + + if ($this->query === $query) { + return $this; + } + + $new = clone $this; + $new->query = $query; + + return $new; + } + + public function withFragment($fragment) + { + $fragment = $this->filterQueryAndFragment($fragment); + + if ($this->fragment === $fragment) { + return $this; + } + + $new = clone $this; + $new->fragment = $fragment; + + return $new; + } + + /** + * Apply parse_url parts to a URI. + * + * @param array $parts Array of parse_url parts to apply. + */ + private function applyParts(array $parts) + { + $this->scheme = isset($parts['scheme']) + ? $this->filterScheme($parts['scheme']) + : ''; + $this->userInfo = isset($parts['user']) + ? $this->filterUserInfoComponent($parts['user']) + : ''; + $this->host = isset($parts['host']) + ? $this->filterHost($parts['host']) + : ''; + $this->port = isset($parts['port']) + ? $this->filterPort($parts['port']) + : null; + $this->path = isset($parts['path']) + ? $this->filterPath($parts['path']) + : ''; + $this->query = isset($parts['query']) + ? $this->filterQueryAndFragment($parts['query']) + : ''; + $this->fragment = isset($parts['fragment']) + ? $this->filterQueryAndFragment($parts['fragment']) + : ''; + if (isset($parts['pass'])) { + $this->userInfo .= ':' . $this->filterUserInfoComponent($parts['pass']); + } + + $this->removeDefaultPort(); + } + + /** + * @param string $scheme + * + * @return string + * + * @throws \InvalidArgumentException If the scheme is invalid. + */ + private function filterScheme($scheme) + { + if (!is_string($scheme)) { + throw new \InvalidArgumentException('Scheme must be a string'); + } + + return \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + } + + /** + * @param string $component + * + * @return string + * + * @throws \InvalidArgumentException If the user info is invalid. + */ + private function filterUserInfoComponent($component) + { + if (!is_string($component)) { + throw new \InvalidArgumentException('User info must be a string'); + } + + return preg_replace_callback( + '/(?:[^%' . self::$charUnreserved . self::$charSubDelims . ']+|%(?![A-Fa-f0-9]{2}))/', + [$this, 'rawurlencodeMatchZero'], + $component + ); + } + + /** + * @param string $host + * + * @return string + * + * @throws \InvalidArgumentException If the host is invalid. + */ + private function filterHost($host) + { + if (!is_string($host)) { + throw new \InvalidArgumentException('Host must be a string'); + } + + return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + } + + /** + * @param int|null $port + * + * @return int|null + * + * @throws \InvalidArgumentException If the port is invalid. + */ + private function filterPort($port) + { + if ($port === null) { + return null; + } + + $port = (int) $port; + if (0 > $port || 0xffff < $port) { + throw new \InvalidArgumentException( + sprintf('Invalid port: %d. Must be between 0 and 65535', $port) + ); + } + + return $port; + } + + /** + * @param UriInterface $uri + * @param array $keys + * + * @return array + */ + private static function getFilteredQueryString(UriInterface $uri, array $keys) + { + $current = $uri->getQuery(); + + if ($current === '') { + return []; + } + + $decodedKeys = array_map('rawurldecode', $keys); + + return array_filter(explode('&', $current), function ($part) use ($decodedKeys) { + return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true); + }); + } + + /** + * @param string $key + * @param string|null $value + * + * @return string + */ + private static function generateQueryString($key, $value) + { + // Query string separators ("=", "&") within the key or value need to be encoded + // (while preventing double-encoding) before setting the query string. All other + // chars that need percent-encoding will be encoded by withQuery(). + $queryString = strtr($key, self::$replaceQuery); + + if ($value !== null) { + $queryString .= '=' . strtr($value, self::$replaceQuery); + } + + return $queryString; + } + + private function removeDefaultPort() + { + if ($this->port !== null && self::isDefaultPort($this)) { + $this->port = null; + } + } + + /** + * Filters the path of a URI + * + * @param string $path + * + * @return string + * + * @throws \InvalidArgumentException If the path is invalid. + */ + private function filterPath($path) + { + if (!is_string($path)) { + throw new \InvalidArgumentException('Path must be a string'); + } + + return preg_replace_callback( + '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', + [$this, 'rawurlencodeMatchZero'], + $path + ); + } + + /** + * Filters the query string or fragment of a URI. + * + * @param string $str + * + * @return string + * + * @throws \InvalidArgumentException If the query or fragment is invalid. + */ + private function filterQueryAndFragment($str) + { + if (!is_string($str)) { + throw new \InvalidArgumentException('Query and fragment must be a string'); + } + + return preg_replace_callback( + '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', + [$this, 'rawurlencodeMatchZero'], + $str + ); + } + + private function rawurlencodeMatchZero(array $match) + { + return rawurlencode($match[0]); + } + + private function validateState() + { + if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) { + $this->host = self::HTTP_DEFAULT_HOST; + } + + if ($this->getAuthority() === '') { + if (0 === strpos($this->path, '//')) { + throw new \InvalidArgumentException('The path of a URI without an authority must not start with two slashes "//"'); + } + if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) { + throw new \InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon'); + } + } elseif (isset($this->path[0]) && $this->path[0] !== '/') { + @trigger_error( + 'The path of a URI with an authority must start with a slash "/" or be empty. Automagically fixing the URI ' . + 'by adding a leading slash to the path is deprecated since version 1.4 and will throw an exception instead.', + E_USER_DEPRECATED + ); + $this->path = '/' . $this->path; + //throw new \InvalidArgumentException('The path of a URI with an authority must start with a slash "/" or be empty'); + } + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/UriNormalizer.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/UriNormalizer.php new file mode 100644 index 0000000..81419ea --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/UriNormalizer.php @@ -0,0 +1,219 @@ +getPath() === '' && + ($uri->getScheme() === 'http' || $uri->getScheme() === 'https') + ) { + $uri = $uri->withPath('/'); + } + + if ($flags & self::REMOVE_DEFAULT_HOST && $uri->getScheme() === 'file' && $uri->getHost() === 'localhost') { + $uri = $uri->withHost(''); + } + + if ($flags & self::REMOVE_DEFAULT_PORT && $uri->getPort() !== null && Uri::isDefaultPort($uri)) { + $uri = $uri->withPort(null); + } + + if ($flags & self::REMOVE_DOT_SEGMENTS && !Uri::isRelativePathReference($uri)) { + $uri = $uri->withPath(UriResolver::removeDotSegments($uri->getPath())); + } + + if ($flags & self::REMOVE_DUPLICATE_SLASHES) { + $uri = $uri->withPath(preg_replace('#//++#', '/', $uri->getPath())); + } + + if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') { + $queryKeyValues = explode('&', $uri->getQuery()); + sort($queryKeyValues); + $uri = $uri->withQuery(implode('&', $queryKeyValues)); + } + + return $uri; + } + + /** + * Whether two URIs can be considered equivalent. + * + * Both URIs are normalized automatically before comparison with the given $normalizations bitmask. The method also + * accepts relative URI references and returns true when they are equivalent. This of course assumes they will be + * resolved against the same base URI. If this is not the case, determination of equivalence or difference of + * relative references does not mean anything. + * + * @param UriInterface $uri1 An URI to compare + * @param UriInterface $uri2 An URI to compare + * @param int $normalizations A bitmask of normalizations to apply, see constants + * + * @return bool + * + * @link https://tools.ietf.org/html/rfc3986#section-6.1 + */ + public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, $normalizations = self::PRESERVING_NORMALIZATIONS) + { + return (string) self::normalize($uri1, $normalizations) === (string) self::normalize($uri2, $normalizations); + } + + private static function capitalizePercentEncoding(UriInterface $uri) + { + $regex = '/(?:%[A-Fa-f0-9]{2})++/'; + + $callback = function (array $match) { + return strtoupper($match[0]); + }; + + return + $uri->withPath( + preg_replace_callback($regex, $callback, $uri->getPath()) + )->withQuery( + preg_replace_callback($regex, $callback, $uri->getQuery()) + ); + } + + private static function decodeUnreservedCharacters(UriInterface $uri) + { + $regex = '/%(?:2D|2E|5F|7E|3[0-9]|[46][1-9A-F]|[57][0-9A])/i'; + + $callback = function (array $match) { + return rawurldecode($match[0]); + }; + + return + $uri->withPath( + preg_replace_callback($regex, $callback, $uri->getPath()) + )->withQuery( + preg_replace_callback($regex, $callback, $uri->getQuery()) + ); + } + + private function __construct() + { + // cannot be instantiated + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/UriResolver.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/UriResolver.php new file mode 100644 index 0000000..a3cb15d --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/UriResolver.php @@ -0,0 +1,222 @@ +getScheme() != '') { + return $rel->withPath(self::removeDotSegments($rel->getPath())); + } + + if ($rel->getAuthority() != '') { + $targetAuthority = $rel->getAuthority(); + $targetPath = self::removeDotSegments($rel->getPath()); + $targetQuery = $rel->getQuery(); + } else { + $targetAuthority = $base->getAuthority(); + if ($rel->getPath() === '') { + $targetPath = $base->getPath(); + $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery(); + } else { + if ($rel->getPath()[0] === '/') { + $targetPath = $rel->getPath(); + } else { + if ($targetAuthority != '' && $base->getPath() === '') { + $targetPath = '/' . $rel->getPath(); + } else { + $lastSlashPos = strrpos($base->getPath(), '/'); + if ($lastSlashPos === false) { + $targetPath = $rel->getPath(); + } else { + $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1) . $rel->getPath(); + } + } + } + $targetPath = self::removeDotSegments($targetPath); + $targetQuery = $rel->getQuery(); + } + } + + return new Uri(Uri::composeComponents( + $base->getScheme(), + $targetAuthority, + $targetPath, + $targetQuery, + $rel->getFragment() + )); + } + + /** + * Returns the target URI as a relative reference from the base URI. + * + * This method is the counterpart to resolve(): + * + * (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) + * + * One use-case is to use the current request URI as base URI and then generate relative links in your documents + * to reduce the document size or offer self-contained downloadable document archives. + * + * $base = new Uri('http://example.com/a/b/'); + * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'. + * echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'. + * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'. + * echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'. + * + * This method also accepts a target that is already relative and will try to relativize it further. Only a + * relative-path reference will be returned as-is. + * + * echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well + * + * @param UriInterface $base Base URI + * @param UriInterface $target Target URI + * + * @return UriInterface The relative URI reference + */ + public static function relativize(UriInterface $base, UriInterface $target) + { + if ($target->getScheme() !== '' && + ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '') + ) { + return $target; + } + + if (Uri::isRelativePathReference($target)) { + // As the target is already highly relative we return it as-is. It would be possible to resolve + // the target with `$target = self::resolve($base, $target);` and then try make it more relative + // by removing a duplicate query. But let's not do that automatically. + return $target; + } + + if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) { + return $target->withScheme(''); + } + + // We must remove the path before removing the authority because if the path starts with two slashes, the URI + // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also + // invalid. + $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost(''); + + if ($base->getPath() !== $target->getPath()) { + return $emptyPathUri->withPath(self::getRelativePath($base, $target)); + } + + if ($base->getQuery() === $target->getQuery()) { + // Only the target fragment is left. And it must be returned even if base and target fragment are the same. + return $emptyPathUri->withQuery(''); + } + + // If the base URI has a query but the target has none, we cannot return an empty path reference as it would + // inherit the base query component when resolving. + if ($target->getQuery() === '') { + $segments = explode('/', $target->getPath()); + $lastSegment = end($segments); + + return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); + } + + return $emptyPathUri; + } + + private static function getRelativePath(UriInterface $base, UriInterface $target) + { + $sourceSegments = explode('/', $base->getPath()); + $targetSegments = explode('/', $target->getPath()); + array_pop($sourceSegments); + $targetLastSegment = array_pop($targetSegments); + foreach ($sourceSegments as $i => $segment) { + if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) { + unset($sourceSegments[$i], $targetSegments[$i]); + } else { + break; + } + } + $targetSegments[] = $targetLastSegment; + $relativePath = str_repeat('../', count($sourceSegments)) . implode('/', $targetSegments); + + // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./". + // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used + // as the first segment of a relative-path reference, as it would be mistaken for a scheme name. + if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) { + $relativePath = "./$relativePath"; + } elseif ('/' === $relativePath[0]) { + if ($base->getAuthority() != '' && $base->getPath() === '') { + // In this case an extra slash is added by resolve() automatically. So we must not add one here. + $relativePath = ".$relativePath"; + } else { + $relativePath = "./$relativePath"; + } + } + + return $relativePath; + } + + private function __construct() + { + // cannot be instantiated + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Utils.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Utils.php new file mode 100644 index 0000000..6b6c8cc --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/Utils.php @@ -0,0 +1,428 @@ + $keys + * + * @return array + */ + public static function caselessRemove($keys, array $data) + { + $result = []; + + foreach ($keys as &$key) { + $key = strtolower($key); + } + + foreach ($data as $k => $v) { + if (!in_array(strtolower($k), $keys)) { + $result[$k] = $v; + } + } + + return $result; + } + + /** + * Copy the contents of a stream into another stream until the given number + * of bytes have been read. + * + * @param StreamInterface $source Stream to read from + * @param StreamInterface $dest Stream to write to + * @param int $maxLen Maximum number of bytes to read. Pass -1 + * to read the entire stream. + * + * @throws \RuntimeException on error. + */ + public static function copyToStream(StreamInterface $source, StreamInterface $dest, $maxLen = -1) + { + $bufferSize = 8192; + + if ($maxLen === -1) { + while (!$source->eof()) { + if (!$dest->write($source->read($bufferSize))) { + break; + } + } + } else { + $remaining = $maxLen; + while ($remaining > 0 && !$source->eof()) { + $buf = $source->read(min($bufferSize, $remaining)); + $len = strlen($buf); + if (!$len) { + break; + } + $remaining -= $len; + $dest->write($buf); + } + } + } + + /** + * Copy the contents of a stream into a string until the given number of + * bytes have been read. + * + * @param StreamInterface $stream Stream to read + * @param int $maxLen Maximum number of bytes to read. Pass -1 + * to read the entire stream. + * + * @return string + * + * @throws \RuntimeException on error. + */ + public static function copyToString(StreamInterface $stream, $maxLen = -1) + { + $buffer = ''; + + if ($maxLen === -1) { + while (!$stream->eof()) { + $buf = $stream->read(1048576); + // Using a loose equality here to match on '' and false. + if ($buf == null) { + break; + } + $buffer .= $buf; + } + return $buffer; + } + + $len = 0; + while (!$stream->eof() && $len < $maxLen) { + $buf = $stream->read($maxLen - $len); + // Using a loose equality here to match on '' and false. + if ($buf == null) { + break; + } + $buffer .= $buf; + $len = strlen($buffer); + } + + return $buffer; + } + + /** + * Calculate a hash of a stream. + * + * This method reads the entire stream to calculate a rolling hash, based + * on PHP's `hash_init` functions. + * + * @param StreamInterface $stream Stream to calculate the hash for + * @param string $algo Hash algorithm (e.g. md5, crc32, etc) + * @param bool $rawOutput Whether or not to use raw output + * + * @return string Returns the hash of the stream + * + * @throws \RuntimeException on error. + */ + public static function hash(StreamInterface $stream, $algo, $rawOutput = false) + { + $pos = $stream->tell(); + + if ($pos > 0) { + $stream->rewind(); + } + + $ctx = hash_init($algo); + while (!$stream->eof()) { + hash_update($ctx, $stream->read(1048576)); + } + + $out = hash_final($ctx, (bool) $rawOutput); + $stream->seek($pos); + + return $out; + } + + /** + * Clone and modify a request with the given changes. + * + * This method is useful for reducing the number of clones needed to mutate + * a message. + * + * The changes can be one of: + * - method: (string) Changes the HTTP method. + * - set_headers: (array) Sets the given headers. + * - remove_headers: (array) Remove the given headers. + * - body: (mixed) Sets the given body. + * - uri: (UriInterface) Set the URI. + * - query: (string) Set the query string value of the URI. + * - version: (string) Set the protocol version. + * + * @param RequestInterface $request Request to clone and modify. + * @param array $changes Changes to apply. + * + * @return RequestInterface + */ + public static function modifyRequest(RequestInterface $request, array $changes) + { + if (!$changes) { + return $request; + } + + $headers = $request->getHeaders(); + + if (!isset($changes['uri'])) { + $uri = $request->getUri(); + } else { + // Remove the host header if one is on the URI + if ($host = $changes['uri']->getHost()) { + $changes['set_headers']['Host'] = $host; + + if ($port = $changes['uri']->getPort()) { + $standardPorts = ['http' => 80, 'https' => 443]; + $scheme = $changes['uri']->getScheme(); + if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) { + $changes['set_headers']['Host'] .= ':' . $port; + } + } + } + $uri = $changes['uri']; + } + + if (!empty($changes['remove_headers'])) { + $headers = self::caselessRemove($changes['remove_headers'], $headers); + } + + if (!empty($changes['set_headers'])) { + $headers = self::caselessRemove(array_keys($changes['set_headers']), $headers); + $headers = $changes['set_headers'] + $headers; + } + + if (isset($changes['query'])) { + $uri = $uri->withQuery($changes['query']); + } + + if ($request instanceof ServerRequestInterface) { + $new = (new ServerRequest( + isset($changes['method']) ? $changes['method'] : $request->getMethod(), + $uri, + $headers, + isset($changes['body']) ? $changes['body'] : $request->getBody(), + isset($changes['version']) + ? $changes['version'] + : $request->getProtocolVersion(), + $request->getServerParams() + )) + ->withParsedBody($request->getParsedBody()) + ->withQueryParams($request->getQueryParams()) + ->withCookieParams($request->getCookieParams()) + ->withUploadedFiles($request->getUploadedFiles()); + + foreach ($request->getAttributes() as $key => $value) { + $new = $new->withAttribute($key, $value); + } + + return $new; + } + + return new Request( + isset($changes['method']) ? $changes['method'] : $request->getMethod(), + $uri, + $headers, + isset($changes['body']) ? $changes['body'] : $request->getBody(), + isset($changes['version']) + ? $changes['version'] + : $request->getProtocolVersion() + ); + } + + /** + * Read a line from the stream up to the maximum allowed buffer length. + * + * @param StreamInterface $stream Stream to read from + * @param int|null $maxLength Maximum buffer length + * + * @return string + */ + public static function readLine(StreamInterface $stream, $maxLength = null) + { + $buffer = ''; + $size = 0; + + while (!$stream->eof()) { + // Using a loose equality here to match on '' and false. + if (null == ($byte = $stream->read(1))) { + return $buffer; + } + $buffer .= $byte; + // Break when a new line is found or the max length - 1 is reached + if ($byte === "\n" || ++$size === $maxLength - 1) { + break; + } + } + + return $buffer; + } + + /** + * Create a new stream based on the input type. + * + * Options is an associative array that can contain the following keys: + * - metadata: Array of custom metadata. + * - size: Size of the stream. + * + * This method accepts the following `$resource` types: + * - `Psr\Http\Message\StreamInterface`: Returns the value as-is. + * - `string`: Creates a stream object that uses the given string as the contents. + * - `resource`: Creates a stream object that wraps the given PHP stream resource. + * - `Iterator`: If the provided value implements `Iterator`, then a read-only + * stream object will be created that wraps the given iterable. Each time the + * stream is read from, data from the iterator will fill a buffer and will be + * continuously called until the buffer is equal to the requested read size. + * Subsequent read calls will first read from the buffer and then call `next` + * on the underlying iterator until it is exhausted. + * - `object` with `__toString()`: If the object has the `__toString()` method, + * the object will be cast to a string and then a stream will be returned that + * uses the string value. + * - `NULL`: When `null` is passed, an empty stream object is returned. + * - `callable` When a callable is passed, a read-only stream object will be + * created that invokes the given callable. The callable is invoked with the + * number of suggested bytes to read. The callable can return any number of + * bytes, but MUST return `false` when there is no more data to return. The + * stream object that wraps the callable will invoke the callable until the + * number of requested bytes are available. Any additional bytes will be + * buffered and used in subsequent reads. + * + * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data + * @param array $options Additional options + * + * @return StreamInterface + * + * @throws \InvalidArgumentException if the $resource arg is not valid. + */ + public static function streamFor($resource = '', array $options = []) + { + if (is_scalar($resource)) { + $stream = self::tryFopen('php://temp', 'r+'); + if ($resource !== '') { + fwrite($stream, $resource); + fseek($stream, 0); + } + return new Stream($stream, $options); + } + + switch (gettype($resource)) { + case 'resource': + /* + * The 'php://input' is a special stream with quirks and inconsistencies. + * We avoid using that stream by reading it into php://temp + */ + $metaData = \stream_get_meta_data($resource); + if (isset($metaData['uri']) && $metaData['uri'] === 'php://input') { + $stream = self::tryFopen('php://temp', 'w+'); + fwrite($stream, stream_get_contents($resource)); + fseek($stream, 0); + $resource = $stream; + } + return new Stream($resource, $options); + case 'object': + if ($resource instanceof StreamInterface) { + return $resource; + } elseif ($resource instanceof \Iterator) { + return new PumpStream(function () use ($resource) { + if (!$resource->valid()) { + return false; + } + $result = $resource->current(); + $resource->next(); + return $result; + }, $options); + } elseif (method_exists($resource, '__toString')) { + return Utils::streamFor((string) $resource, $options); + } + break; + case 'NULL': + return new Stream(self::tryFopen('php://temp', 'r+'), $options); + } + + if (is_callable($resource)) { + return new PumpStream($resource, $options); + } + + throw new \InvalidArgumentException('Invalid resource type: ' . gettype($resource)); + } + + /** + * Safely opens a PHP stream resource using a filename. + * + * When fopen fails, PHP normally raises a warning. This function adds an + * error handler that checks for errors and throws an exception instead. + * + * @param string $filename File to open + * @param string $mode Mode used to open the file + * + * @return resource + * + * @throws \RuntimeException if the file cannot be opened + */ + public static function tryFopen($filename, $mode) + { + $ex = null; + set_error_handler(function () use ($filename, $mode, &$ex) { + $ex = new \RuntimeException(sprintf( + 'Unable to open "%s" using mode "%s": %s', + $filename, + $mode, + func_get_args()[1] + )); + + return true; + }); + + try { + $handle = fopen($filename, $mode); + } catch (\Throwable $e) { + $ex = new \RuntimeException(sprintf( + 'Unable to open "%s" using mode "%s": %s', + $filename, + $mode, + $e->getMessage() + ), 0, $e); + } + + restore_error_handler(); + + if ($ex) { + /** @var $ex \RuntimeException */ + throw $ex; + } + + return $handle; + } + + /** + * Returns a UriInterface for the given value. + * + * This function accepts a string or UriInterface and returns a + * UriInterface for the given value. If the value is already a + * UriInterface, it is returned as-is. + * + * @param string|UriInterface $uri + * + * @return UriInterface + * + * @throws \InvalidArgumentException + */ + public static function uriFor($uri) + { + if ($uri instanceof UriInterface) { + return $uri; + } + + if (is_string($uri)) { + return new Uri($uri); + } + + throw new \InvalidArgumentException('URI must be a string or UriInterface'); + } +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/functions.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/functions.php new file mode 100644 index 0000000..b0901fa --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/functions.php @@ -0,0 +1,422 @@ + '1', 'foo[b]' => '2'])`. + * + * @param string $str Query string to parse + * @param int|bool $urlEncoding How the query string is encoded + * + * @return array + * + * @deprecated parse_query will be removed in guzzlehttp/psr7:2.0. Use Query::parse instead. + */ +function parse_query($str, $urlEncoding = true) +{ + return Query::parse($str, $urlEncoding); +} + +/** + * Build a query string from an array of key value pairs. + * + * This function can use the return value of `parse_query()` to build a query + * string. This function does not modify the provided keys when an array is + * encountered (like `http_build_query()` would). + * + * @param array $params Query string parameters. + * @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986 + * to encode using RFC3986, or PHP_QUERY_RFC1738 + * to encode using RFC1738. + * + * @return string + * + * @deprecated build_query will be removed in guzzlehttp/psr7:2.0. Use Query::build instead. + */ +function build_query(array $params, $encoding = PHP_QUERY_RFC3986) +{ + return Query::build($params, $encoding); +} + +/** + * Determines the mimetype of a file by looking at its extension. + * + * @param string $filename + * + * @return string|null + * + * @deprecated mimetype_from_filename will be removed in guzzlehttp/psr7:2.0. Use MimeType::fromFilename instead. + */ +function mimetype_from_filename($filename) +{ + return MimeType::fromFilename($filename); +} + +/** + * Maps a file extensions to a mimetype. + * + * @param $extension string The file extension. + * + * @return string|null + * + * @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types + * @deprecated mimetype_from_extension will be removed in guzzlehttp/psr7:2.0. Use MimeType::fromExtension instead. + */ +function mimetype_from_extension($extension) +{ + return MimeType::fromExtension($extension); +} + +/** + * Parses an HTTP message into an associative array. + * + * The array contains the "start-line" key containing the start line of + * the message, "headers" key containing an associative array of header + * array values, and a "body" key containing the body of the message. + * + * @param string $message HTTP request or response to parse. + * + * @return array + * + * @internal + * + * @deprecated _parse_message will be removed in guzzlehttp/psr7:2.0. Use Message::parseMessage instead. + */ +function _parse_message($message) +{ + return Message::parseMessage($message); +} + +/** + * Constructs a URI for an HTTP request message. + * + * @param string $path Path from the start-line + * @param array $headers Array of headers (each value an array). + * + * @return string + * + * @internal + * + * @deprecated _parse_request_uri will be removed in guzzlehttp/psr7:2.0. Use Message::parseRequestUri instead. + */ +function _parse_request_uri($path, array $headers) +{ + return Message::parseRequestUri($path, $headers); +} + +/** + * Get a short summary of the message body. + * + * Will return `null` if the response is not printable. + * + * @param MessageInterface $message The message to get the body summary + * @param int $truncateAt The maximum allowed size of the summary + * + * @return string|null + * + * @deprecated get_message_body_summary will be removed in guzzlehttp/psr7:2.0. Use Message::bodySummary instead. + */ +function get_message_body_summary(MessageInterface $message, $truncateAt = 120) +{ + return Message::bodySummary($message, $truncateAt); +} + +/** + * Remove the items given by the keys, case insensitively from the data. + * + * @param iterable $keys + * + * @return array + * + * @internal + * + * @deprecated _caseless_remove will be removed in guzzlehttp/psr7:2.0. Use Utils::caselessRemove instead. + */ +function _caseless_remove($keys, array $data) +{ + return Utils::caselessRemove($keys, $data); +} diff --git a/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/functions_include.php b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/functions_include.php new file mode 100644 index 0000000..96a4a83 --- /dev/null +++ b/user/plugins/automagic-images/vendor/guzzlehttp/psr7/src/functions_include.php @@ -0,0 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/user/plugins/automagic-images/vendor/myclabs/php-enum/src/Enum.php b/user/plugins/automagic-images/vendor/myclabs/php-enum/src/Enum.php new file mode 100644 index 0000000..a16aa23 --- /dev/null +++ b/user/plugins/automagic-images/vendor/myclabs/php-enum/src/Enum.php @@ -0,0 +1,305 @@ + + * @author Daniel Costa + * @author Mirosław Filip + * + * @psalm-template T + * @psalm-immutable + * @psalm-consistent-constructor + */ +abstract class Enum implements \JsonSerializable +{ + /** + * Enum value + * + * @var mixed + * @psalm-var T + */ + protected $value; + + /** + * Enum key, the constant name + * + * @var string + */ + private $key; + + /** + * Store existing constants in a static cache per object. + * + * + * @var array + * @psalm-var array> + */ + protected static $cache = []; + + /** + * Cache of instances of the Enum class + * + * @var array + * @psalm-var array> + */ + protected static $instances = []; + + /** + * Creates a new value of some type + * + * @psalm-pure + * @param mixed $value + * + * @psalm-param T $value + * @throws \UnexpectedValueException if incompatible type is given. + */ + public function __construct($value) + { + if ($value instanceof static) { + /** @psalm-var T */ + $value = $value->getValue(); + } + + $this->key = static::assertValidValueReturningKey($value); + + /** @psalm-var T */ + $this->value = $value; + } + + public function __wakeup() + { + if ($this->key === null) { + $this->key = static::search($this->value); + } + } + + /** + * @param mixed $value + * @return static + * @psalm-return static + */ + public static function from($value): self + { + $key = static::assertValidValueReturningKey($value); + + return self::__callStatic($key, []); + } + + /** + * @psalm-pure + * @return mixed + * @psalm-return T + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns the enum key (i.e. the constant name). + * + * @psalm-pure + * @return string + */ + public function getKey() + { + return $this->key; + } + + /** + * @psalm-pure + * @psalm-suppress InvalidCast + * @return string + */ + public function __toString() + { + return (string)$this->value; + } + + /** + * Determines if Enum should be considered equal with the variable passed as a parameter. + * Returns false if an argument is an object of different class or not an object. + * + * This method is final, for more information read https://github.com/myclabs/php-enum/issues/4 + * + * @psalm-pure + * @psalm-param mixed $variable + * @return bool + */ + final public function equals($variable = null): bool + { + return $variable instanceof self + && $this->getValue() === $variable->getValue() + && static::class === \get_class($variable); + } + + /** + * Returns the names (keys) of all constants in the Enum class + * + * @psalm-pure + * @psalm-return list + * @return array + */ + public static function keys() + { + return \array_keys(static::toArray()); + } + + /** + * Returns instances of the Enum class of all Enum constants + * + * @psalm-pure + * @psalm-return array + * @return static[] Constant name in key, Enum instance in value + */ + public static function values() + { + $values = array(); + + /** @psalm-var T $value */ + foreach (static::toArray() as $key => $value) { + $values[$key] = new static($value); + } + + return $values; + } + + /** + * Returns all possible values as an array + * + * @psalm-pure + * @psalm-suppress ImpureStaticProperty + * + * @psalm-return array + * @return array Constant name in key, constant value in value + */ + public static function toArray() + { + $class = static::class; + + if (!isset(static::$cache[$class])) { + /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ + $reflection = new \ReflectionClass($class); + /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ + static::$cache[$class] = $reflection->getConstants(); + } + + return static::$cache[$class]; + } + + /** + * Check if is valid enum value + * + * @param $value + * @psalm-param mixed $value + * @psalm-pure + * @psalm-assert-if-true T $value + * @return bool + */ + public static function isValid($value) + { + return \in_array($value, static::toArray(), true); + } + + /** + * Asserts valid enum value + * + * @psalm-pure + * @psalm-assert T $value + */ + public static function assertValidValue($value): void + { + self::assertValidValueReturningKey($value); + } + + /** + * Asserts valid enum value + * + * @psalm-pure + * @psalm-assert T $value + */ + private static function assertValidValueReturningKey($value): string + { + if (false === ($key = static::search($value))) { + throw new \UnexpectedValueException("Value '$value' is not part of the enum " . static::class); + } + + return $key; + } + + /** + * Check if is valid enum key + * + * @param $key + * @psalm-param string $key + * @psalm-pure + * @return bool + */ + public static function isValidKey($key) + { + $array = static::toArray(); + + return isset($array[$key]) || \array_key_exists($key, $array); + } + + /** + * Return key for value + * + * @param mixed $value + * + * @psalm-param mixed $value + * @psalm-pure + * @return string|false + */ + public static function search($value) + { + return \array_search($value, static::toArray(), true); + } + + /** + * Returns a value when called statically like so: MyEnum::SOME_VALUE() given SOME_VALUE is a class constant + * + * @param string $name + * @param array $arguments + * + * @return static + * @throws \BadMethodCallException + * + * @psalm-pure + */ + public static function __callStatic($name, $arguments) + { + $class = static::class; + if (!isset(self::$instances[$class][$name])) { + $array = static::toArray(); + if (!isset($array[$name]) && !\array_key_exists($name, $array)) { + $message = "No static method or enum constant '$name' in class " . static::class; + throw new \BadMethodCallException($message); + } + return self::$instances[$class][$name] = new static($array[$name]); + } + return clone self::$instances[$class][$name]; + } + + /** + * Specify data which should be serialized to JSON. This method returns data that can be serialized by json_encode() + * natively. + * + * @return mixed + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + * @psalm-pure + */ + public function jsonSerialize() + { + return $this->getValue(); + } +} diff --git a/user/plugins/automagic-images/vendor/myclabs/php-enum/src/PHPUnit/Comparator.php b/user/plugins/automagic-images/vendor/myclabs/php-enum/src/PHPUnit/Comparator.php new file mode 100644 index 0000000..302bf80 --- /dev/null +++ b/user/plugins/automagic-images/vendor/myclabs/php-enum/src/PHPUnit/Comparator.php @@ -0,0 +1,54 @@ +register(new \MyCLabs\Enum\PHPUnit\Comparator()); + */ +final class Comparator extends \SebastianBergmann\Comparator\Comparator +{ + public function accepts($expected, $actual) + { + return $expected instanceof Enum && ( + $actual instanceof Enum || $actual === null + ); + } + + /** + * @param Enum $expected + * @param Enum|null $actual + * + * @return void + */ + public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false) + { + if ($expected->equals($actual)) { + return; + } + + throw new ComparisonFailure( + $expected, + $actual, + $this->formatEnum($expected), + $this->formatEnum($actual), + false, + 'Failed asserting that two Enums are equal.' + ); + } + + private function formatEnum(Enum $enum = null) + { + if ($enum === null) { + return "null"; + } + + return get_class($enum)."::{$enum->getKey()}()"; + } +} diff --git a/user/plugins/automagic-images/vendor/paquettg/php-html-parser/SECURITY.md b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/SECURITY.md new file mode 100644 index 0000000..3fc4dfc --- /dev/null +++ b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +We only support the most recent version with security fixes. + +## Reporting a Vulnerability + +If you have found any issues that might have security implications, please refer to https://tidelift.com/security + +Do not report security reports publicly. diff --git a/user/plugins/automagic-images/vendor/paquettg/php-html-parser/composer.json b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/composer.json new file mode 100644 index 0000000..166886f --- /dev/null +++ b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/composer.json @@ -0,0 +1,38 @@ +{ + "name": "paquettg/php-html-parser", + "type": "library", + "description": "An HTML DOM parser. It allows you to manipulate HTML. Find tags on an HTML page with selectors just like jQuery.", + "keywords": ["html", "dom", "parser"], + "homepage": "https://github.com/paquettg/php-html-parser", + "license": "MIT", + "authors": [ + { + "name": "Gilles Paquette", + "email": "paquettg@gmail.com", + "homepage": "http://gillespaquette.ca" + } + ], + "require": { + "php": ">=7.2", + "ext-mbstring": "*", + "ext-zlib": "*", + "ext-curl": "*", + "paquettg/string-encode": "~1.0.0", + "php-http/httplug": "^2.1", + "guzzlehttp/guzzle": "^7.0", + "guzzlehttp/psr7": "^1.6", + "myclabs/php-enum": "^1.7" + }, + "require-dev": { + "phpunit/phpunit": "^7.5.1", + "mockery/mockery": "^1.2", + "infection/infection": "^0.13.4", + "phan/phan": "^2.4", + "friendsofphp/php-cs-fixer": "^2.16" + }, + "autoload": { + "psr-4": { + "PHPHtmlParser\\": "src/PHPHtmlParser" + } + } +} diff --git a/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Content.php b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Content.php new file mode 100644 index 0000000..f133217 --- /dev/null +++ b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Content.php @@ -0,0 +1,257 @@ +'; + protected $slash = " />\r\n\t"; + protected $attr = ' >'; + + /** + * Content constructor. + */ + public function __construct(string $content = '') + { + $this->content = $content; + $this->size = \strlen($content); + $this->pos = 0; + } + + /** + * Returns the current position of the content. + */ + public function getPosition(): int + { + return $this->pos; + } + + /** + * Gets the current character we are at. + * + * @param ?int $char + */ + public function char(?int $char = null): string + { + return $this->content[$char ?? $this->pos] ?? ''; + } + + /** + * Gets a string from the current character position. + * + * @param int $length + * @return string + */ + public function string(int $length = 1): string + { + $string = ''; + $position = $this->pos; + do { + $string .= $this->char($position++); + } while ($position < $this->pos + $length); + return $string; + } + + /** + * Moves the current position forward. + * + * @throws ContentLengthException + */ + public function fastForward(int $count): Content + { + if (!$this->canFastForward($count)) { + // trying to go over the content length, throw exception + throw new ContentLengthException('Attempt to fastForward pass the length of the content.'); + } + $this->pos += $count; + + return $this; + } + + /** + * Checks if we can move the position forward. + */ + public function canFastForward(int $count): bool + { + return \strlen($this->content) >= $this->pos + $count; + } + + /** + * Moves the current position backward. + */ + public function rewind(int $count): Content + { + $this->pos -= $count; + if ($this->pos < 0) { + $this->pos = 0; + } + + return $this; + } + + /** + * Copy the content until we find the given string. + */ + public function copyUntil(string $string, bool $char = false, bool $escape = false): string + { + if ($this->pos >= $this->size) { + // nothing left + return ''; + } + + if ($escape) { + $position = $this->pos; + $found = false; + while (!$found) { + $position = \strpos($this->content, $string, $position); + if ($position === false) { + // reached the end + break; + } + + if ($this->char($position - 1) == '\\') { + // this character is escaped + ++$position; + continue; + } + + $found = true; + } + } elseif ($char) { + $position = \strcspn($this->content, $string, $this->pos); + $position += $this->pos; + } else { + $position = \strpos($this->content, $string, $this->pos); + } + + if ($position === false) { + // could not find character, just return the remaining of the content + $return = \substr($this->content, $this->pos, $this->size - $this->pos); + if ($return === false) { + throw new LogicalException('Substr returned false with position ' . $this->pos . '.'); + } + $this->pos = $this->size; + + return $return; + } + + if ($position == $this->pos) { + // we are at the right place + return ''; + } + + $return = \substr($this->content, $this->pos, $position - $this->pos); + if ($return === false) { + throw new LogicalException('Substr returned false with position ' . $this->pos . '.'); + } + // set the new position + $this->pos = $position; + + return $return; + } + + /** + * Copies the content until the string is found and return it + * unless the 'unless' is found in the substring. + */ + public function copyUntilUnless(string $string, string $unless): string + { + $lastPos = $this->pos; + $this->fastForward(1); + $foundString = $this->copyUntil($string, true, true); + + $position = \strcspn($foundString, $unless); + if ($position == \strlen($foundString)) { + return $string . $foundString; + } + // rewind changes and return nothing + $this->pos = $lastPos; + + return ''; + } + + /** + * Copies the content until it reaches the token string.,. + * + * @uses $this->copyUntil() + */ + public function copyByToken(StringToken $stringToken, bool $char = false, bool $escape = false): string + { + $string = $stringToken->getValue(); + + return $this->copyUntil($string, $char, $escape); + } + + /** + * Skip a given set of characters. + * + * @throws LogicalException + */ + public function skip(string $string, bool $copy = false): string + { + $len = \strspn($this->content, $string, $this->pos); + if ($len === false) { + throw new LogicalException('Strspn returned false with position ' . $this->pos . '.'); + } + $return = ''; + if ($copy) { + $return = \substr($this->content, $this->pos, $len); + if ($return === false) { + throw new LogicalException('Substr returned false with position ' . $this->pos . '.'); + } + } + + // update the position + $this->pos += $len; + + return $return; + } + + /** + * Skip a given token of pre-defined characters. + * + * @uses $this->skip() + */ + public function skipByToken(StringToken $skipToken, bool $copy = false): string + { + $string = $skipToken->getValue(); + + return $this->skip($string, $copy); + } +} diff --git a/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Contracts/Dom/CleanerInterface.php b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Contracts/Dom/CleanerInterface.php new file mode 100644 index 0000000..725a8a4 --- /dev/null +++ b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Contracts/Dom/CleanerInterface.php @@ -0,0 +1,16 @@ +parsedSelectorDTO[] = $parsedSelectorDTO; + } + } + } + + /** + * @param ParsedSelectorDTO[] $parsedSelectorDTOs + */ + public static function makeCollection(array $parsedSelectorDTOs): ParsedSelectorCollectionDTO + { + return new ParsedSelectorCollectionDTO($parsedSelectorDTOs); + } + + /** + * @return ParsedSelectorDTO[] + */ + public function getParsedSelectorDTO(): array + { + return $this->parsedSelectorDTO; + } +} diff --git a/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Selector/ParsedSelectorDTO.php b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Selector/ParsedSelectorDTO.php new file mode 100644 index 0000000..bce0721 --- /dev/null +++ b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Selector/ParsedSelectorDTO.php @@ -0,0 +1,41 @@ +rules[] = $ruleDTO; + } + } + } + + /** + * @param RuleDTO[] $ruleDTOs + */ + public static function makeFromRules(array $ruleDTOs): ParsedSelectorDTO + { + return new ParsedSelectorDTO($ruleDTOs); + } + + /** + * @return RuleDTO[] + */ + public function getRules(): array + { + return $this->rules; + } +} diff --git a/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Selector/RuleDTO.php b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Selector/RuleDTO.php new file mode 100644 index 0000000..5299e3a --- /dev/null +++ b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Selector/RuleDTO.php @@ -0,0 +1,100 @@ +tag = $values['tag']; + $this->operator = $values['operator']; + $this->key = $values['key']; + $this->value = $values['value']; + $this->noKey = $values['noKey']; + $this->alterNext = $values['alterNext']; + } + + /** + * @param string|array|null $key + * @param string|array|null $value + */ + public static function makeFromPrimitives(string $tag, string $operator, $key, $value, bool $noKey, bool $alterNext): RuleDTO + { + return new RuleDTO([ + 'tag' => $tag, + 'operator' => $operator, + 'key' => $key, + 'value' => $value, + 'noKey' => $noKey, + 'alterNext' => $alterNext, + ]); + } + + public function getTag(): string + { + return $this->tag; + } + + public function getOperator(): string + { + return $this->operator; + } + + /** + * @return string|array|null + */ + public function getKey() + { + return $this->key; + } + + /** + * @return string|array|null + */ + public function getValue() + { + return $this->value; + } + + public function isNoKey(): bool + { + return $this->noKey; + } + + public function isAlterNext(): bool + { + return $this->alterNext; + } +} diff --git a/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Tag/AttributeDTO.php b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Tag/AttributeDTO.php new file mode 100644 index 0000000..3e7e182 --- /dev/null +++ b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/Tag/AttributeDTO.php @@ -0,0 +1,60 @@ +value = $values['value']; + $this->doubleQuote = $values['doubleQuote'] ?? true; + } + + public static function makeFromPrimitives(?string $value, bool $doubleQuote = true): AttributeDTO + { + return new AttributeDTO([ + 'value' => $value, + 'doubleQuote' => $doubleQuote, + ]); + } + + public function getValue(): ?string + { + return $this->value; + } + + public function isDoubleQuote(): bool + { + return $this->doubleQuote; + } + + public function htmlspecialcharsDecode(): void + { + if (!\is_null($this->value)) { + $this->value = \htmlspecialchars_decode($this->value); + } + } + + /** + * @throws Exception + */ + public function encodeValue(Encode $encode) + { + $this->value = $encode->convert($this->value); + } +} diff --git a/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/TagDTO.php b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/TagDTO.php new file mode 100644 index 0000000..71f0ec1 --- /dev/null +++ b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/DTO/TagDTO.php @@ -0,0 +1,74 @@ +status = $values['status'] ?? false; + $this->closing = $values['closing'] ?? false; + $this->node = $values['node'] ?? null; + $this->tag = $values['tag'] ?? null; + } + + public static function makeFromPrimitives(bool $status = false, bool $closing = false, ?HtmlNode $node = null, ?string $tag = null): TagDTO + { + return new TagDTO([ + 'status' => $status, + 'closing' => $closing, + 'node' => $node, + 'tag' => $tag, + ]); + } + + public function isStatus(): bool + { + return $this->status; + } + + public function isClosing(): bool + { + return $this->closing; + } + + /** + * @return mixed + */ + public function getNode(): ?HtmlNode + { + return $this->node; + } + + /** + * @return mixed + */ + public function getTag(): ?string + { + return $this->tag; + } +} diff --git a/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Discovery/CleanerDiscovery.php b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Discovery/CleanerDiscovery.php new file mode 100644 index 0000000..96ef678 --- /dev/null +++ b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Discovery/CleanerDiscovery.php @@ -0,0 +1,25 @@ +domParser = $domParser; + $this->domCleaner = $domCleaner; + } + + /** + * Returns the inner html of the root node. + * + * @throws ChildNotFoundException + * @throws UnknownChildTypeException + * @throws NotLoadedException + */ + public function __toString(): string + { + $this->isLoaded(); + + return $this->root->innerHtml(); + } + + /** + * Loads the dom from a document file/url. + * + * @throws ChildNotFoundException + * @throws CircularException + * @throws Exceptions\ContentLengthException + * @throws LogicalException + * @throws StrictException + */ + public function loadFromFile(string $file, ?Options $options = null): Dom + { + $content = @\file_get_contents($file); + if ($content === false) { + throw new LogicalException('file_get_contents failed and returned false when trying to read "' . $file . '".'); + } + + return $this->loadStr($content, $options); + } + + /** + * Use a curl interface implementation to attempt to load + * the content from a url. + * + * @throws ChildNotFoundException + * @throws CircularException + * @throws Exceptions\ContentLengthException + * @throws LogicalException + * @throws StrictException + * @throws ClientExceptionInterface + */ + public function loadFromUrl(string $url, ?Options $options = null, ?ClientInterface $client = null, ?RequestInterface $request = null): Dom + { + if ($client === null) { + $client = new Client(); + } + if ($request === null) { + $request = new Request('GET', $url); + } + + $response = $client->sendRequest($request); + $content = $response->getBody()->getContents(); + + return $this->loadStr($content, $options); + } + + /** + * Parsers the html of the given string. Used for load(), loadFromFile(), + * and loadFromUrl(). + * + * @throws ChildNotFoundException + * @throws CircularException + * @throws Exceptions\ContentLengthException + * @throws LogicalException + * @throws StrictException + */ + public function loadStr(string $str, ?Options $options = null): Dom + { + $localOptions = new Options(); + if ($this->globalOptions !== null) { + $localOptions = $localOptions->setFromOptions($this->globalOptions); + } + if ($options !== null) { + $localOptions = $localOptions->setFromOptions($options); + } + + $html = $this->domCleaner->clean($str, $localOptions, $this->defaultCharset); + + $this->content = new Content($html); + + $this->root = $this->domParser->parse($localOptions, $this->content, \strlen($str)); + $this->domParser->detectCharset($localOptions, $this->defaultCharset, $this->root); + + return $this; + } + + /** + * Sets a global options array to be used by all load calls. + */ + public function setOptions(Options $options): Dom + { + $this->globalOptions = $options; + + return $this; + } + + /** + * Find elements by css selector on the root node. + * + * @throws NotLoadedException + * @throws ChildNotFoundException + * + * @return mixed|Collection|null + */ + public function find(string $selector, int $nth = null) + { + $this->isLoaded(); + + return $this->root->find($selector, $nth); + } + + /** + * Simple wrapper function that returns an element by the + * id. + * + * @param $id + * + * @throws NotLoadedException + * @throws ChildNotFoundException + * + * @return mixed|Collection|null + */ + public function getElementById($id) + { + $this->isLoaded(); + + return $this->find('#' . $id, 0); + } + + /** + * Simple wrapper function that returns all elements by + * tag name. + * + * @throws NotLoadedException + * @throws ChildNotFoundException + * + * @return mixed|Collection|null + */ + public function getElementsByTag(string $name) + { + $this->isLoaded(); + + return $this->find($name); + } + + /** + * Simple wrapper function that returns all elements by + * class name. + * + * @throws NotLoadedException + * @throws ChildNotFoundException + * + * @return mixed|Collection|null + */ + public function getElementsByClass(string $class) + { + $this->isLoaded(); + + return $this->find('.' . $class); + } + + /** + * Checks if the load methods have been called. + * + * @throws NotLoadedException + */ + private function isLoaded(): void + { + if (\is_null($this->content)) { + throw new NotLoadedException('Content is not loaded!'); + } + } +} diff --git a/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Cleaner.php b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Cleaner.php new file mode 100644 index 0000000..52e5728 --- /dev/null +++ b/user/plugins/automagic-images/vendor/paquettg/php-html-parser/src/PHPHtmlParser/Dom/Cleaner.php @@ -0,0 +1,130 @@ +isCleanupInput()) { + // skip entire cleanup step + return $str; + } + + // check if the string is gziped + $is_gzip = 0 === \mb_strpos($str, "\x1f" . "\x8b" . "\x08", 0, 'US-ASCII'); + if ($is_gzip) { + $str = \gzdecode($str); + if ($str === false) { + throw new LogicalException('gzdecode returned false. Error when trying to decode the string.'); + } + } + + // we must handle character encoding + $str = $this->setUpRegexEncoding($str, $options, $defaultCharset); + + // remove white space before closing tags + $str = \mb_eregi_replace("'\s+>", "'>", $str); + if ($str === false) { + throw new LogicalException('mb_eregi_replace returned false instead of a string. Error when attempting to clean single quotes.'); + } + $str = \mb_eregi_replace('"\s+>', '">', $str); + if ($str === false) { + throw new LogicalException('mb_eregi_replace returned false instead of a string. Error when attempting to clean double quotes.'); + } + + // clean out the \n\r + $replace = ' '; + if ($options->isPreserveLineBreaks()) { + $replace = ' '; + } + $str = \str_replace(["\r\n", "\r", "\n"], $replace, $str); + if ($str === false) { + throw new LogicalException('str_replace returned false instead of a string. Error when attempting to clean input string.'); + } + + // strip the doctype + $str = \mb_eregi_replace('', '', $str); + if ($str === false) { + throw new LogicalException('mb_eregi_replace returned false instead of a string. Error when attempting to strip the doctype.'); + } + + // strip out comments + $str = \mb_eregi_replace('', '', $str); + if ($str === false) { + throw new LogicalException('mb_eregi_replace returned false instead of a string. Error when attempting to strip comments.'); + } + + // strip out cdata + $str = \mb_eregi_replace("", '', $str); + if ($str === false) { + throw new LogicalException('mb_eregi_replace returned false instead of a string. Error when attempting to strip out cdata.'); + } + + // strip out ` tag at the bottom with this code: + + ```html + + ``` + +# File Upload + +To upload files you can use the `file` form field. [The standard features apply](https://learn.getgrav.org/forms/blueprints/how-to-add-file-upload), and you can simply edit your custom blueprint with a field definition similar to: + +``` + item_image: + type: file + label: Item Image + random_name: true + destination: 'user/data/flex-objects/files' + multiple: true +``` + +> In order to fully take advantage of image uploads, you should always be using `FolderStorage`, meaning that the objects get saved to individual folders together with the images. Other storage layers may or may not support media. + +# Advanced + +You can radically alter the structure of the `contacts.json` data file by making major edits to the `contacts.yaml` blueprint file. However, it's best to start with an empty `contacts.json` if you are making wholesale changes or you will have data conflicts. Best to create your blueprint first. Reloading a **New Entry** until the form looks correct, then try saving, and check to make sure the stored `user/data/flex-objects/contacts.json` file looks correct. + +Then you will need to make more widespread changes to the site Twig templates. You might need to adjust the number of columns and the field names. You will also need to pay attention to the JavaScript initialization in each template. + +# Features + +Here are the main benefits of using Flex objects: + +* CRUD is automatically handled for you by Flex Objects plugin +* Objects can be stored using many different strategies, including single file, file per object or folder per object; using yaml, json etc. +* Flex types can be easily extended by custom PHP collection and object classes +* Both Flex objects and collections know how to render themselves: `echo $object->render($layout, $context)` or `{% render object layout: layout with context %}` +* You can easily create custom layouts for your objects and collections to be used in different pages +* Both Flex objects and collections support serialization and `json_encode()` +* Flex objects support Grav `Medium` objects with few lines of code +* Flex objects can have relations to other Flex objects with few lines of code defining the relation +* Flex directories support indexes which allow searching objects without loading all of them +* Efficient caching for indexes, searches, objects and rendered output + +# Limitations and future improvements + +Right now there are a few limitations: + +* Frontend only has a basic routing for the individual pages (you need to do the advanced routing manually by yourself) +* Administration needs more features like filtering, bulk updates etc +* It would be nice to have an easy way to display Flex admin in other admin plugins (it is already possible, but not easy) +* Optional database storage layer would be nice to have +* We need general collection functions to do simple filtering, like: "display all published items" without custom PHP code + +### Notes: + +1. You can actually use pretty much any folder under the `user/` folder of Grav. Simply edit the **Extra Admin Twig Path** option in the `flex-objects.yaml` file. It defaults to `theme://admin/templates` which means it uses the default theme's `admin/templates/` folder if it exists. +2. You can use any path for front end Twig templates also, if you don't want to put them in your theme, you can add an entry in the **Extra Site Twig Path** option of the `flex-objects.yaml` configuration and point to another location. + +# Tricks and tips + +* You can enable and disable directories from **Plugins** > **Flex Objects** + * New Flex Directories can be registered by simply creating a new blueprint file in `user/blueprints/flex-objects` folder + * You can also add types from your plugins by hooking into `onFlexInit` event (see `AccountsServiceProvider` in Grav) +* To properly create your own custom types, you need at least the object blueprint and the template files for collections and objects +* Use `flex-objects.md` page to create entry point for your own directory + * In page header you can use nested `flex.directory` variable to define the directory (or do it in admin) + * In Admin you can just select the directory under the page title + + +# Parameters supported by Flex page type: + +``` +--- +title: 'Flex Directories' +flex: + directories: + layout: default + list: + - accounts + - contacts +--- +``` + +`directories.layout`: uses template file `templates/flex-objects/directories/[LAYOUT].html.twig` +`directories.list`: list of flex directories displayed in this page diff --git a/user/plugins/flex-objects/admin/pages/flex-objects.md b/user/plugins/flex-objects/admin/pages/flex-objects.md new file mode 100644 index 0000000..c9359a2 --- /dev/null +++ b/user/plugins/flex-objects/admin/pages/flex-objects.md @@ -0,0 +1,7 @@ +--- +title: Flex Objects + +access: + admin.flex-objects: true + admin.super: true +--- \ No newline at end of file diff --git a/user/plugins/flex-objects/admin/templates/flex-objects.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects.html.twig new file mode 100644 index 0000000..74ee1ca --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects.html.twig @@ -0,0 +1,30 @@ +{%- set user = admin.user -%} +{%- set route = controller.route -%} +{%- set type = directory.config('admin.template') ?? target -%} + +{# Set action from ?preview=1 #} +{%- if key and uri.currentUri().queryParam('preview') %} + {% set action = 'preview' %} +{% endif -%} + +{%- set template -%} + {%- if action == 'add' -%} + edit + {%- elseif action == 'delete' -%} + list + {%- else -%} + {{- action ?: task ?: 'types' -}} + {%- endif -%} +{%- endset -%} + +{%- set separator = config.system.param_sep -%} +{%- set view_config = directory.config('admin.views.' ~ template) ?? directory.config('admin.' ~ template) ?? [] -%} + +{%- include target ? [ + 'flex-objects/types/' ~ type ~ '/' ~ template ~ '.html.twig', + 'flex-objects/types/default/' ~ template ~ '.html.twig', + 'flex-objects/layouts/404.html.twig' + ] : [ + 'flex-objects/types/default/' ~ template ~ '.html.twig', + 'flex-objects/layouts/404.html.twig' + ] -%} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects.json.twig b/user/plugins/flex-objects/admin/templates/flex-objects.json.twig new file mode 100644 index 0000000..c5b85dc --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects.json.twig @@ -0,0 +1 @@ +{{- admin.json_response|json_encode|raw -}} \ No newline at end of file diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/layouts/404.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/layouts/404.html.twig new file mode 100644 index 0000000..6101c81 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/layouts/404.html.twig @@ -0,0 +1 @@ +{{ 'PLUGIN_FLEX_OBJECTS.ERROR.LAYOUT_NOT_FOUND'|tu(template, null) }} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/layouts/accounts/partials/top.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/layouts/accounts/partials/top.html.twig new file mode 100644 index 0000000..46b1ef6 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/layouts/accounts/partials/top.html.twig @@ -0,0 +1,25 @@ +{% set active_html = 'class="active"' %} +{% set is_configure = route.gravParam('') is same as('configure') %} +{% set authorize = directory.config('admin.views.configure.authorize') ?? directory.config('admin.configure.authorize') ?? 'admin.super' %} + +{% if allowed %} +

    +{% endif %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/add.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/add.html.twig new file mode 100644 index 0000000..076f293 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/add.html.twig @@ -0,0 +1,3 @@ + + {{ 'PLUGIN_ADMIN.ADD'|tu }} + diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/back.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/back.html.twig new file mode 100644 index 0000000..dbb57c6 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/back.html.twig @@ -0,0 +1,3 @@ + + {{ "PLUGIN_ADMIN.BACK"|tu }} + diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/configuration.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/configuration.html.twig new file mode 100644 index 0000000..2a463b2 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/configuration.html.twig @@ -0,0 +1,7 @@ +{%- set authorize = directory.config('admin.views.configure.authorize') ?? directory.config('admin.configure.authorize') ?? 'admin.super' %} + +{%- if configure_url and user.authorize(authorize) %} + + {{ 'PLUGIN_ADMIN.CONFIGURATION'|tu }} + +{% endif %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/delete.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/delete.html.twig new file mode 100644 index 0000000..779760e --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/delete.html.twig @@ -0,0 +1,3 @@ + + {{ 'PLUGIN_ADMIN.DELETE'|tu }} + diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export-csv.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export-csv.html.twig new file mode 100644 index 0000000..8c53c1b --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export-csv.html.twig @@ -0,0 +1,3 @@ + + {{ 'PLUGIN_FLEX_OBJECTS.CSV'|tu }} + diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export.html.twig new file mode 100644 index 0000000..0f8382b --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/export.html.twig @@ -0,0 +1,21 @@ +{% if export.options %} +
    + + +
    + +{% else %} + + {{ export.title ?? (export.formatter.class ? 'PLUGIN_ADMIN.EXPORT'|tu : 'PLUGIN_FLEX_OBJECTS.CSV'|tu) }} + +{% endif %} \ No newline at end of file diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/languages.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/languages.html.twig new file mode 100644 index 0000000..58c2551 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/languages.html.twig @@ -0,0 +1,18 @@ +
    + + {% if admin_languages|length > (language in admin_languages)|int %} + + + {% endif %} +
    diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview-open.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview-open.html.twig new file mode 100644 index 0000000..df8893d --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview-open.html.twig @@ -0,0 +1,5 @@ +{% if preview_url %} + + {{ "PLUGIN_ADMIN.OPEN_NEW_TAB"|tu }} + +{% endif %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview.html.twig new file mode 100644 index 0000000..6307913 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/preview.html.twig @@ -0,0 +1,3 @@ + + {{ "PLUGIN_ADMIN.PREVIEW"|tu }} + diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/save.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/save.html.twig new file mode 100644 index 0000000..379d97d --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/buttons/save.html.twig @@ -0,0 +1,4 @@ +{% set task = task ?? 'save' %} + diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/configure.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/configure.html.twig new file mode 100644 index 0000000..4495527 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/configure.html.twig @@ -0,0 +1,103 @@ +{% extends 'partials/base.html.twig' %} +{% use 'flex-objects/types/default/titlebar/configure.html.twig' %} + +{% set name = view_config['form'] %} +{% set form = form ?? directory.directoryForm(name) %} + +{# Allowed actions #} +{% set can_save = can_save ?? user.authorize(view_config['authorize'] ?? 'admin.super') %} + +{# These variables can be overridden from the main template file #} +{% set allowed = allowed ?? (directory and form and can_save) %} +{% set back_route = back_route ?? ('/' ~ route.getRoute(1)) %} +{% set title_icon = title_icon ?? view_config['icon'] ?? 'fa-cog' %} +{% set title -%} + {%- set title_config = view_config['title'] ?? null -%} + {%- if title_config.template -%} + {{- include(template_from_string(title_config.template, 'configure title template')) -}} + {%- else -%} + {{- directory.title }} {{ 'PLUGIN_ADMIN.CONFIGURATION'|tu -}} + {% endif %} +{%- endset %} + +{% macro spanToggle(input, length) %} + {{ (repeat('  ', (length - input|length) / 2) ~ input ~ repeat('  ', (length - input|length) / 2))|raw }} +{% endmacro %} +{% import _self as macro %} + +{% block body %} + {% set back_url = back_url ?? admin_route(back_route) %} + + {{ parent() }} +{% endblock body %} + +{% block content_top %} + {% if allowed and user.authorize('admin.super') %} + {% set save_location = directory.getDirectoryConfigUri(name) %} +
    {{ "PLUGIN_ADMIN.SAVE_LOCATION"|tu }}: {{ url(save_location, false, true)|trim('/') }}
    + {% endif %} +{% endblock %} + +{% block topbar %} + {% if user.authorize('admin.super') %} +
    + {% set normalText = 'PLUGIN_ADMIN.NORMAL'|tu %} + {% set expertText = 'PLUGIN_ADMIN.EXPERT'|tu %} + {% set maxLen = max([normalText|length, expertText|length]) %} + {% set normalText = macro.spanToggle(normalText, maxLen) %} + {% set expertText = macro.spanToggle(expertText, maxLen) %} + +
    + + + + + +
    +
    + {% endif %} +{% endblock topbar %} + +{% block content %} + {{ parent() }} + + {% if allowed %} +
    +
    + {# TODO: RAW MODE +
    + {{ block('topbar') }} +
    + #} + {% block edit %} + {% include 'partials/blueprints.html.twig' with { form: form, data: form.data } %} + {% endblock %} +
    +
    + + {% include 'partials/modal-changes-detected.html.twig' %} + + {% else %} + + {% do page.modifyHeader('http_response_code', 404) %} +
    +

    {{ 'PLUGIN_ADMIN.ERROR'|tu }} 404

    +
    +

    + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_NOT_EXIST'|tu }} +

    +
    +
    + + {% endif %} +{% endblock %} + +{% block bottom %} + {{ parent() }} + + {# TODO: RAW MODE + + #} +{% endblock bottom %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/edit.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/edit.html.twig new file mode 100644 index 0000000..b14310d --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/edit.html.twig @@ -0,0 +1,121 @@ +{% extends 'partials/base.html.twig' %} +{% use 'flex-objects/types/default/titlebar/edit.html.twig' %} + +{# Avoid defining form and object twice: object should always come from the form! #} +{% if form is not defined %} + {% set form = object.form %} + {% set object = form.object %} +{% endif %} + +{# Allowed actions #} +{% set can_list = can_list ?? directory.isAuthorized('list', 'admin', user) %} +{% set can_read = can_read ?? (object.exists ? object.isAuthorized('read', 'admin', user) : directory.isAuthorized('create', 'admin', user)) %} +{% set can_create = can_create ?? object.isAuthorized('create', 'admin', user) %} +{% set can_save = can_save ?? (object.exists ? object.isAuthorized('update', 'admin', user) : directory.isAuthorized('create', 'admin', user)) %} +{% set can_delete = can_delete ?? (object.exists and object.isAuthorized('delete', 'admin', user)) %} +{% set can_translate = can_translate ?? (admin.multilang and object.hasFlexFeature('flex-translate')) %} +{% set can_preview = can_preview ?? (can_read and object.exists and (directory.config('admin.views.preview.enabled') ?? directory.config('admin.preview.enabled', false))) %} + +{# Translations #} +{% if can_translate %} + {% set translate_include_default = translate_include_default ?? grav.config.get('system.languages.include_default_lang_file_extension', true) %} + {% set all_languages = grav.admin.siteLanguages %} + {% set admin_languages = admin.languages_enabled %} + {% set default_language = grav.language.default %} + {% set object_language = object.language %} + {% set language = controller.language %} + {% set has_translation = object.hasTranslation(language, false) %} + + {# + {% if translate_include_default %} + {% set all_languages = all_languages|merge({'': 'Default'}) %} + {% set admin_languages = admin_languages|merge({'': ''}) %} + {% set object_languages = object.languages(true) %} + {% else %} + #} + {% set language = language ?: default_language %} + {% set object_language = object_language ?: default_language %} + {% set object_languages = object.languages(false) %} + {# endif #} +{% endif %} + +{# These variables can be overridden from the main template file #} +{% set allowed = allowed ?? (directory and (object.exists and (can_read or can_save)) or (action == 'add' and can_read)) %} +{% set back_route = back_route ?? ('/' ~ (action != 'edit' and not key ? route.getRoute(1, not can_list ? -1 : null) : route.getRoute(1, not can_list ? -2 : -1))) %} +{% set title_icon = title_icon ?? view_config['icon'] ?? directory.config.admin.menu.list.icon ?? 'fa-file-text-o' %} +{% set title -%} + {%- set title_config = view_config['title'] -%} + {%- if title_config.template -%} + {{- include(template_from_string(title_config.template, 'edit title template')) -}} + {%- else -%} + {{- title ?? object.form.getValue('title') ?? object.title ?? key -}} + {% endif %} +{%- endset %} + +{% block body %} + {% set back_url = back_url ?? admin_route(back_route) %} + {% set id = key %} + {% set blueprint = blueprint ?? form.blueprint %} + + {{ parent() }} +{% endblock body %} + +{% block content_top %} + {% if allowed and user.authorize('admin.super') %} + {% if directory and object or action == 'add' %} + {% set save_location = object.getStorageFolder() ?? directory.getStorageFolder() %} +
    {{ "PLUGIN_ADMIN.SAVE_LOCATION"|tu }}: {{ url(save_location, false, true)|trim('/') }} {{ not object.exists ? '[NEW]' }}
    + {% endif %} + {% endif %} + {% if object.exists and form.flash.exists %} +
    + {{ 'PLUGIN_FLEX_OBJECTS.STATE.EDITING_DRAFT'|tu }} +
    + {% endif %} +{% endblock %} + +{% block content %} + {% if allowed %} +
    +
    +
    + {% block topbar %}{% endblock %} +
    + {% block edit %} + {% include 'partials/blueprints.html.twig' with { form: form, context: object, data: object } %} + {% endblock %} +
    +
    + + {% include 'partials/modal-changes-detected.html.twig' %} + + {% if can_delete %} + {% include ['flex-objects/types/' ~ target ~ '/modals/remove.html.twig', 'flex-objects/types/default/modals/remove.html.twig'] with { name: target } %} + {% endif %} + + {% elseif (object.exists) %} + + {% do page.modifyHeader('http_response_code', 403) %} +
    +

    {{ 'PLUGIN_ADMIN.ERROR'|tu }} 403

    +
    +

    + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_FORBIDDEN'|tu }} +

    +
    +
    + + {% else %} + + {% do page.modifyHeader('http_response_code', 404) %} +
    +

    {{ 'PLUGIN_ADMIN.ERROR'|tu }} 404

    +
    +

    + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_NOT_EXIST'|tu }} +

    +
    +
    + + {% endif %} +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/list.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/list.html.twig new file mode 100644 index 0000000..3da6805 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/list.html.twig @@ -0,0 +1,98 @@ +{% extends 'partials/base.html.twig' %} +{% use 'flex-objects/types/default/titlebar/list.html.twig' %} + +{# Allowed actions #} +{% set export = directory.config('admin.views.export') ?? directory.config('admin.export') ?? [] %} +{% set can_export = can_export ?? (export['enabled'] ?? export|array|count)|bool %} +{% set can_create = can_create ?? directory.isAuthorized('create', 'admin', user) %} +{% set can_translate = can_translate ?? (admin.multilang and directory.object.hasFlexFeature('flex-translate')) %} + +{% set per_page = per_page ?? grav.uri.currentUri.queryParam('per_page') %} + +{# Translations #} +{% if can_translate %} + {% set translate_include_default = translate_include_default ?? grav.config.get('system.languages.include_default_lang_file_extension', true) %} + {% set all_languages = grav.admin.siteLanguages %} + {% set admin_languages = admin.languages_enabled %} + {% set default_language = grav.language.default %} + {% set language = controller.language %} + {# + {% if translate_include_default %} + {% set all_languages = all_languages|merge({'': 'Default'}) %} + {% set admin_languages = admin_languages|merge({'': ''}) %} + {% else %} + #} + {% set language = language ?: default_language %} + {# endif #} +{% endif %} + +{# These variables can be overridden from the main template file #} +{% set allowed = allowed ?? (directory and directory.isAuthorized('list', 'admin', user)) %} +{% set back_route = back_route ?? route.getRoute(1, -1) %} + +{% set configure_path = directory.config('admin.router.actions.configure.path') %} +{% set configure_route = configure_route ?? (configure_path ? route.withRoute(admin_route(configure_path)|trim('/')) : null) %} +{% set configure_route = configure_route ?? route.withGravParam('', 'configure') %} + +{% set title_icon = title_icon ?? view_config['icon'] ?? directory.config.admin.menu.list.icon ?? 'fa-file-text-o' %} +{% set title -%} + {%- set title_config = view_config['title'] ?? null -%} + {%- if title_config.template -%} + {{- include(template_from_string(title_config.template, 'configure title template')) -}} + {%- else -%} + {{- directory.title -}} + {% endif %} +{%- endset %} + +{% set schema = directory.blueprint.schema %} + +{% do assets.addJs('plugin://flex-objects/js/flex-objects.js', { 'group': 'bottom', 'loading': 'defer' }) %} + +{% block body %} + {% set collection = directory ? collection.isAuthorized('list', 'admin', user) %} + {% set directory_config = view_config['options'] ?? config.get('plugins.flex-objects.admin_list', { per_page: 15, order: { by: 'updated_timestamp', dir: 'desc' }}) %} + {% set per_page = max(1, per_page ?? directory_config.per_page) %} + {% set table = directory ? flex.dataTable(collection.flexDirectory(), { collection: collection, limit: per_page, sort: directory_config.order.by ~ '|' ~ directory_config.order.dir }) %} + {% set back_url = admin_route(back_route) %} + {% set configure_url = (directory.config('admin.views.configure.hidden') ?? directory.config('admin.configure.hidden', false)) is not same as(true) ? configure_route.toString(true) %} + + {% set fields = table.getColumns() %} + {% set fields_count = fields ? count(fields) : 0 %} + {% set fields_width = 8 %} + {% set fields_set = 0 %} + {% set title_field = view_config['title'] %} + {% for key,options in fields %} + {% set fields_width = fields_width + options.width ?: 0 %} + {% set fields_set = fields_set + (options.width ? 1 : 0) %} + {% if not title_field and options.link == 'edit' %} + {% set title_field = key %} + {% endif %} + {% endfor %} + + {{ parent() }} +{% endblock body %} + +{% block content_top %} +{% if allowed and user.authorize('admin.super') %} + {% set save_location = directory.getStorageFolder() %} +
    {{ "PLUGIN_ADMIN.SAVE_LOCATION"|tu }}: {{ url(save_location, false, true)|trim('/') }}
    +{% endif %} +{% endblock %} + +{% block content %} +{% if allowed %} + {% block content_list %} + {% include ['flex-objects/types/' ~ target ~ '/list/list.html.twig', 'flex-objects/types/default/list/list.html.twig'] %} + {% endblock %} +{% else %} + {% do page.modifyHeader('http_response_code', 404) %} +
    +

    {{ 'PLUGIN_ADMIN.ERROR'|tu }} 404

    +
    +

    + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_NOT_EXIST'|tu }} +

    +
    +
    +{% endif %} +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list.html.twig new file mode 100644 index 0000000..4f90ad9 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list.html.twig @@ -0,0 +1,112 @@ +{% block directory %} +
    + {% if not fields %} + {% block no_list %} +
    +

    {{ 'PLUGIN_FLEX_OBJECTS.ERROR.BLUEPRINT_NO_LIST'|tu( target, null )|raw }}

    +
      +
    • + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.BLUEPRINT_NO_LIST_ADVISE'|tu }} +
    • +
    • + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.BLUEPRINT_NO_LIST_TEMPLATE'|tu( target, null )|raw }} +
    • +
    +
    + {% endblock %} + {% elseif not collection.count %} + {% block no_entries %} +
    + {% if directory.isAuthorized('create', 'admin', user) %} + {% set createLink = admin_route(flex.adminRoute(collection, {action: 'add'})) %} + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.LIST_EMPTY_ADD'|tu(createLink, null)|raw }} + {% else %} + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.LIST_EMPTY'|tu }} + {% endif %} +
    + {% endblock %} + {% else %} + {% block entries %} + {% set per_page = per_page ?? directory_config.per_page %} + + {% set tableFields = [] %} + {% set searchFields = [] %} + {% for key, options in fields %} + {% set name = key %} + {% set sortField = options.sort.field ?? key %} + {% set title = (options.field.label ?? schema.get(options.alias ?? key).label)|tu %} + {% set width = options.width ?: ((100-fields_width) / ((fields_count-fields_set) ?: 1))|round(3) %} + {% set title_class = options.title_class ?: '' %} + {% set data_class = options.data_class ?: '' %} + + {# Vuetable doesn't like field names with `.` in them, so we convert name and sortField to `_` #} + {% set tableFields = tableFields|merge([ + { + name: name|replace({'.': '_'}), + sortField: sortField, + title: title ?? 'N/A', + width: width ~ '%', + titleClass: title_class, + dataClass: data_class + } + ]) %} + + {# FIXME: Search fields should be passed and individually customizable, right now defaulting to all fields selected #} + {% set searchFields = searchFields|merge([key|replace({'.': '_'})]) %} + {% endfor %} + {% set tableFields = tableFields|merge([{ name: '_actions_', title: 'Actions', titleClass: 'right' }]) %} + + + {% set list = table.jsonSerialize %} + +
    + + {% for i in 0..((min(per_page, list.data|count) + 3) - 1) %} + + + + + + + + + + + + + + + + + + + + + + + + {% endfor %} + +
    + {% endblock %} + {% endif %} + + {% block modals %} + {% include 'flex-objects/types/default/modals/remove.html.twig' with { name: target } %} + {% endblock %} +
    +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list_actions.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list_actions.html.twig new file mode 100644 index 0000000..e4d9da5 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/list/list_actions.html.twig @@ -0,0 +1,39 @@ +{% set object_title = title_field ? "'" ~ object[title_field]|join(' ') ~ "'" : 'Item' %} +{% set can_read = object.isAuthorized('read', 'admin', user) %} +{% set can_update = object.isAuthorized('update', 'admin', user) %} +{% set can_delete = object.isAuthorized('delete', 'admin', user) %} + +{% if can_read and object.getRoute() %} + {% block action_preview %} + + + + {% endblock %} +{% endif %} + +{% if can_update %} + {% block action_edit %} + + + + {% endblock %} +{% elseif can_read %} + {% block action_read %} + + + + {% endblock %} +{% endif %} + +{% if can_delete %} + {% block action_delete %} + + + + {% endblock %} +{% endif %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/modals/remove.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/modals/remove.html.twig new file mode 100644 index 0000000..f37500a --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/modals/remove.html.twig @@ -0,0 +1,13 @@ +
    +
    + {# FIXME -name|singularize- is not translatable #} +

    {{ 'PLUGIN_FLEX_OBJECTS.ACTION.DELETE_N'|tu }} {{ name|singularize|capitalize }}

    +

    + {{ 'PLUGIN_FLEX_OBJECTS.ACTION.REALLY_DELETE'|tu( name|singularize, null ) }} +

    +
    + + {{ "PLUGIN_ADMIN.CONTINUE"|tu }} +
    +
    +
    diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/preview.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/preview.html.twig new file mode 100644 index 0000000..3e24744 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/preview.html.twig @@ -0,0 +1,62 @@ +{% extends 'partials/base.html.twig' %} +{% use 'flex-objects/types/default/titlebar/preview.html.twig' %} + +{# Allowed actions #} +{% set can_preview = can_preview ?? (object.exists and (view_config['enabled'] ?? false)) %} +{% set can_translate = can_translate ?? (admin.multilang and object.hasFlexFeature('flex-translate')) %} + +{# These variables can be overridden from the main template file #} +{% set allowed = allowed ?? (directory and (object.exists or action == 'add')) %} +{% set back_route = back_route ?? ('/' ~ route.getRoute(1)) %} +{% set title_icon = title_icon ?? view_config['icon'] ?? directory.config.admin.menu.list.icon ?? 'fa-file-text-o' %} +{% set title -%} + {%- set title_config = view_config['title'] -%} + {%- if title_config.template -%} + {{- include(template_from_string(title_config.template, 'edit title template')) -}} + {%- else -%} + {{- title ?? object.form.getValue('title') ?? object.title ?? key -}} + {% endif %} +{%- endset %} +{% set preview_url -%} + {%- set route_config = view_config['route'] -%} + {%- if route_config.template -%} + {{- include(template_from_string(route_config.template, 'preview route template')) -}} + {%- else -%} + {{- preview_url ?? object.getRoute().uri ?: '' -}} + {%- endif -%} +{% endset -%} + +{% block body %} + {% if not can_preview or not preview_url %} + {% set allowed = false %} + {% endif %} + {% set id = key %} + {% set blueprint = object.blueprint ?? directory.blueprint %} + {% set back_url = back_url ?? admin_route(back_route) %} + + {{ parent() }} +{% endblock body %} + +{% block content_wrapper %} +{% if can_preview and allowed and preview_url %} +
    +
    + +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock content_wrapper %} + +{% block content %} + {% do page.modifyHeader('http_response_code', 404) %} +
    +

    {{ 'PLUGIN_ADMIN.ERROR'|tu }} 404

    +
    +

    + {{ 'PLUGIN_FLEX_OBJECTS.ERROR.PAGE_NOT_EXIST'|tu }} +

    +
    +
    +{% endblock content %} \ No newline at end of file diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/configure.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/configure.html.twig new file mode 100644 index 0000000..08d6f86 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/configure.html.twig @@ -0,0 +1,32 @@ +{% block titlebar %} + {% block titlebar_button_bar %} +
    + {# BACK #} + {% block back_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/default/buttons/back.html.twig'] %} + {% endblock back_button %} + + {% block extra_buttons %}{% endblock extra_buttons %} + + {# SAVE #} + {% if can_save %} + {% block save_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/save.html.twig', 'flex-objects/types/default/buttons/save.html.twig'] with {task: 'configure'} %} + {% endblock save_button %} + {% endif %} +
    + {% endblock titlebar_button_bar %} + + {% block titlebar_title %} +

    + {% if allowed %} + + {{ title }} + {% else %} + + {{ 'PLUGIN_ADMIN.ERROR'|tu }} +   {% endif %} +

    + {% endblock titlebar_title %} + +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/edit.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/edit.html.twig new file mode 100644 index 0000000..c0b0de6 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/edit.html.twig @@ -0,0 +1,46 @@ +{% block titlebar %} + {% block titlebar_button_bar %} +
    + {# BACK #} + {% block back_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/default/buttons/back.html.twig'] %} + {% endblock back_button %} + + {# PREVIEW #} + {% if can_preview %} + {% block preview_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/preview.html.twig', 'flex-objects/types/default/buttons/preview.html.twig'] %} + {% endblock preview_button %} + {% endif %} + + {# DELETE #} + {% if can_delete %} + {% block delete_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/delete.html.twig', 'flex-objects/types/default/buttons/delete.html.twig'] %} + {% endblock delete_button %} + {% endif %} + + {% block extra_buttons %}{% endblock extra_buttons %} + + {# SAVE #} + {% if allowed and can_save %} + {% block save_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/save.html.twig', 'flex-objects/types/default/buttons/save.html.twig'] with {task: 'save'} %} + {% endblock save_button %} + {% endif %} +
    + {% endblock titlebar_button_bar %} + + {% block titlebar_title %} +

    + {% if allowed %} + + {{ not object.exists ? '[NEW]' }} {{ title }} + {% else %} + + {{ 'PLUGIN_ADMIN.ERROR'|tu }} + {% endif %} +

    + {% endblock titlebar_title %} + +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/list.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/list.html.twig new file mode 100644 index 0000000..03a0188 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/list.html.twig @@ -0,0 +1,48 @@ +{% block titlebar %} + {% block titlebar_button_bar %} +
    + {# BACK #} + {% block back_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/default/buttons/back.html.twig'] %} + {% endblock back_button %} + + {# EXPORT #} + {% if can_export %} + {% block export_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/export.html.twig', 'flex-objects/types/default/buttons/export.html.twig'] with {export: directory.config('admin.views.export') ?? directory.config('admin.export') ?? []} %} + {% endblock export_button %} + {% endif %} + + {# CREATE #} + {% if can_create %} + {% block create_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/add.html.twig', 'flex-objects/types/default/buttons/add.html.twig'] %} + {% endblock create_button %} + {% endif %} + + {# LANGUAGES #} + {% if can_translate %} + {% block languages_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/languages.html.twig', 'flex-objects/types/default/buttons/languages.html.twig'] %} + {% endblock languages_button %} + {% endif %} + + {# CONFIGURE #} + {% block configure %} + {% include 'flex-objects/types/default/buttons/configuration.html.twig' %} + {% endblock configure %} +
    + {% endblock titlebar_button_bar %} + + {% block titlebar_title %} +

    + {% if allowed %} + + {{ directory ? title|tu : 'Error' }} + {% else %} + + {{ 'PLUGIN_ADMIN.ERROR'|tu }} +  {% endif %} +

    + {% endblock titlebar_title %} +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/preview.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/preview.html.twig new file mode 100644 index 0000000..d50cbe7 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/preview.html.twig @@ -0,0 +1,30 @@ +{% block titlebar %} + {% block titlebar_button_bar %} +
    + {# BACK #} + {% block back_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/default/buttons/back.html.twig'] %} + {% endblock back_button %} + + {# PREVIEW #} + {% if can_preview %} + {% block preview_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/preview-open.html.twig', 'flex-objects/types/default/buttons/preview-open.html.twig'] %} + {% endblock preview_button %} + {% endif %} +
    + {% endblock titlebar_button_bar %} + + {% block titlebar_title %} +

    + {% if allowed %} + + {{ "PLUGIN_ADMIN.PREVIEW"|tu }}: {{ not object.exists ? '[NEW]' }} {{ title }} + {% else %} + + {{ 'PLUGIN_ADMIN.ERROR'|tu }} + {% endif %} +

    + {% endblock titlebar_title %} + +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/types.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/types.html.twig new file mode 100644 index 0000000..97789de --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/titlebar/types.html.twig @@ -0,0 +1,22 @@ +{% block titlebar %} + {% block titlebar_button_bar %} +
    + {# BACK #} + {% block back_button %} + {% include 'flex-objects/types/default/buttons/back.html.twig' %} + {% endblock back_button %} + + {# CONFIGURE #} + {% block configure %} + {% include 'flex-objects/types/default/buttons/configuration.html.twig' %} + {% endblock configure %} +
    + {% endblock titlebar_button_bar %} + + {% block titlebar_title %} +

    + + {{ "PLUGIN_FLEX_OBJECTS.TITLE"|tu }} +

    + {% endblock titlebar_title %} +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/default/types.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/types.html.twig new file mode 100644 index 0000000..88b1ab2 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/default/types.html.twig @@ -0,0 +1,46 @@ +{% extends 'partials/base.html.twig' %} +{% use 'flex-objects/types/default/titlebar/types.html.twig' %} + +{% set flex = grav['flex_objects'] %} + +{# These variables can be overridden from the main template file #} +{% set back_route = back_route ?? ('/' ~ route.getRoute(1, -1)) %} +{% set configure_route = '/plugins/flex-objects' %} + +{% block body %} + {% set back_url = admin_route(back_route) %} + {% set configure_url = configure_route ? admin_route(configure_route) : null %} + + {{ parent() }} +{% endblock body %} + +{% block content %} + +

    {{ 'PLUGIN_FLEX_OBJECTS.TYPES_TITLE'|tu }}

    + +
    + {% for name,directory in flex.directories if directory.enabled and directory.config('admin.hidden', false) is not same as(true) and not directory.config('admin.menu') %} + {% try %} + {% set collection = directory.collection %} + {% if flex.adminRoute(collection) %} +
    + +

    {{ directory.title|tu }} {{ collection.isAuthorized('list', 'admin', user).count }}

    +

    + {{ directory.description }} +

    +
    + {% endif %} + {% catch %} +
    +

    {{ 'PLUGIN_FLEX_OBJECTS.ERROR.BAD_DIRECTORY'|tu }} '{{ name }}'

    +

    + {{ e.message }} +

    +
    + {% endcatch %} + {% endfor %} + +
    + +{% endblock %} \ No newline at end of file diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/add.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/add.html.twig new file mode 100644 index 0000000..74242aa --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/add.html.twig @@ -0,0 +1,20 @@ +
    + + + +
    diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/back.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/back.html.twig new file mode 100644 index 0000000..df0761a --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/back.html.twig @@ -0,0 +1,3 @@ + + + diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/copy.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/copy.html.twig new file mode 100644 index 0000000..47ec501 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/copy.html.twig @@ -0,0 +1,4 @@ +{# href="{{ uri.addNonce(route.withoutParams().withGravParam('task', 'copy').getUri(), 'admin-form', 'admin-nonce') }}" #} + + {{ "PLUGIN_ADMIN.COPY"|tu }} + diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/delete.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/delete.html.twig new file mode 100644 index 0000000..859b38b --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/delete.html.twig @@ -0,0 +1,3 @@ + + {{ "PLUGIN_ADMIN.DELETE"|tu }} + diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/move.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/move.html.twig new file mode 100644 index 0000000..17f607c --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/move.html.twig @@ -0,0 +1,6 @@ + + {{ "PLUGIN_ADMIN.MOVE"|tu }} + +
    + {% include 'partials/page-move.html.twig' with { blueprints: admin.blueprints('admin/pages/move'), data: context } %} +
    \ No newline at end of file diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-child.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-child.html.twig new file mode 100644 index 0000000..778ccac --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-child.html.twig @@ -0,0 +1,9 @@ +{% if child_url %} + + + +{% else %} + + + +{% endif %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-next.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-next.html.twig new file mode 100644 index 0000000..c38113a --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-next.html.twig @@ -0,0 +1,9 @@ +{% if next_url %} + + + +{% else %} + + + +{% endif %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-parent.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-parent.html.twig new file mode 100644 index 0000000..b57f851 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-parent.html.twig @@ -0,0 +1,9 @@ +{% if parent_url %} + + + +{% else %} + + + +{% endif %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-prev.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-prev.html.twig new file mode 100644 index 0000000..141e9b2 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/nav-prev.html.twig @@ -0,0 +1,9 @@ +{% if prev_url %} + + + +{% else %} + + + +{% endif %} \ No newline at end of file diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/preview.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/preview.html.twig new file mode 100644 index 0000000..7445322 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/preview.html.twig @@ -0,0 +1,5 @@ +{% if object.routable and object.published %} + + + +{% endif %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/save.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/save.html.twig new file mode 100644 index 0000000..fd401bc --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/buttons/save.html.twig @@ -0,0 +1,23 @@ +{% set task = task ?? 'save' %} +
    + + {% if can_translate %} + {% set untranslated = admin_languages|array_diff(object_languages|merge([language])) %} + {% if count(untranslated) %} + + + {% endif %} + {% endif %} +
    diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/edit.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/edit.html.twig new file mode 100644 index 0000000..baf5c87 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/edit.html.twig @@ -0,0 +1,236 @@ +{% extends 'flex-objects/types/default/edit.html.twig' %} + +{# Avoid defining form and object twice: object should always come from the form! #} +{% set expert = user.authorize('admin.super') and admin.session.expert != '0' %} +{% if expert or form is not defined %} + {% set form = object.form(expert ? 'raw' : '') %} + {% set object = form.object %} +{% endif %} + +{% set title = title ?? form.getValue('header.title') ?? object.title ?? key %} +{% set parent = object.parent %} +{% set can_read = can_read ?? (object.exists ? object.isAuthorized('read', 'admin', user) : object.isAuthorized('create', 'admin', user))|bool %} +{% set can_copy = can_copy ?? (parent.exists and parent.isAuthorized('create', 'admin', user)) %} +{% set can_create = can_create ?? (object.exists and object.isAuthorized('create', 'admin', user)) %} +{% set can_save = can_save ?? (object.exists ? object.isAuthorized('update', 'admin', user) : object.isAuthorized('create', 'admin', user))|bool %} +{% set can_move = can_move ?? can_save and form.blueprint.schema.property('route').type is same as('parents') %} +{% set can_translate = can_translate ?? (admin.multilang and object.hasFlexFeature('page-translate') and not object.root()) %} + +{% macro spanToggle(input, length) %} + {{ (repeat('  ', (length - input|length) / 2) ~ input ~ repeat('  ', (length - input|length) / 2))|raw }} +{% endmacro %} +{% import _self as macro %} + +{% block body %} + {% set current_route = '/' ~ route.getRoute(1) %} + + {% if not object.root() %} + {% set child = object.children.first %} + {% set prev = object.prevSibling %} + {% set next = object.nextSibling %} + + {% set parent_url = parent and not parent.root ? admin_route(back_route) %} + {% set child_url = can_read and child ? admin_route(current_route ~ '/' ~ child.slug) %} + {% set prev_url = can_read and prev ? admin_route(back_route ~ '/' ~ prev.slug) %} + {% set next_url = can_read and next ? admin_route(back_route ~ '/' ~ next.slug) %} + {% endif %} + {% set back_url = back_url ?? admin_route(flex.adminRoute(directory.getFlexType())) %} + + {{ parent() }} +{% endblock body %} + +{% block back_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/pages/buttons/back.html.twig'] + with { back_url: back_url } %} + {% if not object.root() %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/nav-prev.html.twig', 'flex-objects/types/pages/buttons/nav-prev.html.twig'] + with { prev_url: prev_url, title: prev.route } %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/nav-parent.html.twig', 'flex-objects/types/pages/buttons/nav-parent.html.twig'] + with { parent_url: parent_url, title: parent.route } %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/nav-child.html.twig', 'flex-objects/types/pages/buttons/nav-child.html.twig'] + with { child_url: child_url, title: child.route } %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/nav-next.html.twig', 'flex-objects/types/pages/buttons/nav-next.html.twig'] + with { next_url: next_url, title: next.route } %} + {% endif %} +{% endblock back_button %} + +{% block preview_button %} + {% if object.exists and not object.root() %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/preview.html.twig', 'flex-objects/types/pages/buttons/preview.html.twig'] %} + {% endif %} +{% endblock preview_button %} + +{% block delete_button %} + {# FIXME: add support for deleting root file only #} + {% if not object.root() %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/delete.html.twig', 'flex-objects/types/pages/buttons/delete.html.twig'] %} + {% endif %} +{% endblock delete_button %} + +{% block extra_buttons %} + {% if object.exists and not object.root() %} + {% if can_create %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/add.html.twig', 'flex-objects/types/pages/buttons/add.html.twig'] %} + {% endif %} + {% if can_copy %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/copy.html.twig', 'flex-objects/types/pages/buttons/copy.html.twig'] %} + {% endif %} + {% if can_move %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/move.html.twig', 'flex-objects/types/pages/buttons/move.html.twig'] %} + {% endif %} + {% endif %} +{% endblock extra_buttons %} + +{% block save_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/save.html.twig', 'flex-objects/types/pages/buttons/save.html.twig'] %} +{% endblock save_button %} + +{% block content_top %} + {% if allowed and user.authorize('admin.super') %} +
    + {% set save_location = object.getStorageFolder() ?: directory.getStorageFolder() %} + {{ "PLUGIN_ADMIN.SAVE_LOCATION"|tu }}: {{ url(save_location, false, true)|trim('/') }} {{ not object.exists ? '[NEW]' }} (type: {{ (form.getValue('name') ?: 'default') }}) +
    + {% endif %} + {% if object.exists and form.flash.exists %} +
    + {{ 'PLUGIN_FLEX_OBJECTS.STATE.EDITING_DRAFT'|tu }} +
    + {% endif %} + {% if not object.exists %} +
    + {{ 'PLUGIN_FLEX_OBJECTS.STATE.NOT_CREATED_YET'|tu }} +
    + {% elseif can_translate %} + {% set is_default = language is same as(default_language) %} + {% if is_default and default_language in object_languages %} + {% if not translate_include_default and object.property('lang') %} + {# Handle default language extension #} +
    + {% set overrideLanguage = all_languages[object_language] ?? object_language %} + {{ 'PLUGIN_FLEX_OBJECTS.LANGUAGE.USING_OVERRIDE'|tu(overrideLanguage, null)|raw }} + {{ object.hasTranslation('', false) ? 'PLUGIN_FLEX_OBJECTS.LANGUAGE.UNUSED_DEFAULT'|tu|raw }} +
    + {% elseif translate_include_default %} + {% if not object.property('lang') %} +
    + {{ 'PLUGIN_FLEX_OBJECTS.LANGUAGE.USING_DEFAULT'|tu|raw }} +
    + {% elseif object.hasTranslation('', false) %} +
    + {{ 'PLUGIN_FLEX_OBJECTS.LANGUAGE.UNUSED_DEFAULT'|tu|raw }} +
    + {% endif %} + {% endif %} + {% elseif not has_translation %} +
    + {% set overrideLanguage = all_languages[language] ?? object_language %} + {{ 'PLUGIN_FLEX_OBJECTS.LANGUAGE.NOT_TRANSLATED_YET'|tu(overrideLanguage, null)|raw }} + {% if language == object_language %} + {{ 'PLUGIN_FLEX_OBJECTS.LANGUAGE.NO_FALLBACK_FOUND'|tu|raw }} + {% else %} + {% set overrideLanguage = all_languages[object_language] ?? object_language %} + {{ 'PLUGIN_FLEX_OBJECTS.LANGUAGE.FALLING_BACK'|tu(overrideLanguage, null)|raw }} + {% endif %} +
    + {% endif %} + {% endif %} +{% endblock content_top %} + +{% block topbar %} + {% if can_translate %} +
    + + {% if count(object_languages) > (object_language in object_languages)|int %} + + + {% endif %} +
    + {% endif %} + + {% if user.authorize('admin.super') %} +
    + {% set normalText = 'PLUGIN_ADMIN.NORMAL'|tu %} + {% set expertText = 'PLUGIN_ADMIN.EXPERT'|tu %} + {% set maxLen = max([normalText|length, expertText|length]) %} + {% set normalText = macro.spanToggle(normalText, maxLen) %} + {% set expertText = macro.spanToggle(expertText, maxLen) %} + +
    + + + + + +
    +
    + {% endif %} +{% endblock topbar %} + +{% block edit %} + {% include 'partials/blueprints.html.twig' with { context: object, data: object, blueprints: form.blueprint } %} +{% endblock edit %} + +{% block content %} + {{ parent() }} + + {% include 'partials/modal-changes-detected.html.twig' %} + + {% if object.exists %} + {% set modal_data = data({ + route: '/' ~ object.key, + name: object.header.child_type ?? object.blueprint.child_type ?? 'default' + }) %} + +
    + {% include 'partials/blueprints-new.html.twig' with { form: null, blueprints: admin.blueprints('admin/pages/new'), data: modal_data, form_id: 'new-page' } %} +
    + +
    + {% include 'partials/blueprints-new-folder.html.twig' with { form: null, blueprints: admin.blueprints('admin/pages/new_folder'), data: modal_data, form_id: 'new-folder' } %} +
    + +
    + {% include 'partials/blueprints-new.html.twig' with { form: null, blueprints: admin.blueprints('admin/pages/modular_new'), data: modal_data, form_id: 'new-module' } %} +
    + +
    + {% include 'partials/blueprints-copy.html.twig' with { blueprints: admin.blueprints('admin/pages/copy'), data: data({ title: object.title ~ ' (Copy)', folder: object.slug ~ '-copy' }), form_id: 'copy' } %} +
    + {% endif %} + + {# TODO: regular pages support extra modals from admin config #} + +
    +
    +

    {{ 'PLUGIN_FLEX_OBJECTS.PARENTS'|tu }}

    +
    {{ 'PLUGIN_FLEX_OBJECTS.STATE.LOADING'|tu }}
    +
    + +
    +
    + +{% endblock content %} + +{% block bottom %} + {{ parent() }} + +{% endblock bottom %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/list.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/list.html.twig new file mode 100644 index 0000000..e4ab2f5 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/list.html.twig @@ -0,0 +1,29 @@ +{% extends 'flex-objects/types/default/list.html.twig' %} + +{% set can_create = true %} + +{% block back_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/pages/buttons/back.html.twig'] %} +{% endblock back_button %} + +{% block create_button %} + {% for key, add_modal in config.plugins.admin.add_modals %} + {% if add_modal.show_in|default('bar') == 'bar' %} + {{ add_modal.label|tu }} + {% endif %} + {% endfor %} + + {% include ['flex-objects/types/' ~ target ~ '/buttons/add.html.twig', 'flex-objects/types/pages/buttons/add.html.twig'] %} +{% endblock %} + +{% block content_top %}{% endblock %} + +{% block content_list %} + {% set list_layout = grav.uri.param('layout', 'columns') %} + {% include [ + 'flex-objects/types/' ~ target ~ '/list/' ~ list_layout ~ '.html.twig', + 'flex-objects/types/pages/list/' ~ list_layout ~ '.html.twig', + 'flex-objects/types/' ~ target ~ '/list/list.html.twig', + 'flex-objects/types/pages/list/list.html.twig' + ] %} +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/list/columns.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/list/columns.html.twig new file mode 100644 index 0000000..082bb61 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/list/columns.html.twig @@ -0,0 +1,155 @@ +{% macro toggle(id, title, filters, name = null) %} + {% set name = id|fieldName %} + {% set filter = filters[name] ?? null %} + {% set value = filter is null ? 0 : (not filter)|int+1 %} + {% set classes = ['status-unchecked', 'status-checked', 'status-indeterminate'] %} + + + + + +{% endmacro %} + +{% import _self as macros %} + +{% block directory %} + {% set filters = grav.request.getCookieParams()['grav-admin-flexpages']|base64_decode|json_decode(true)['filters'] %} + {% set hidePanel = filters|length == 0 or (filters|length == 1 and filters['filters[search]']) %} +
    +
    +
    + + +
    +
    + + {{ 'PLUGIN_FLEX_OBJECTS.FILTER.PAGE_ATTRIBUTES'|tu }} + + {{ macros.toggle('filters.routable', 'Routable', filters) }} + {{ macros.toggle('filters.module', 'Module', filters) }} + {{ macros.toggle('filters.visible', 'Visible', filters) }} + {{ macros.toggle('filters.published', 'Published', filters) }} + {{ macros.toggle('filters.translated', 'Translated', filters) }} + {{ macros.toggle('filters.folder', 'Empty Folder', filters) }} +
    + + {% set selected = filters['filters[page_type]']|split(',') %} + {% set page_types = admin.types(null) %} {# directory.config('filters.ignore_page_types') #} +
    + + {{ 'PLUGIN_FLEX_OBJECTS.FILTER.PAGE_TYPES'|tu }} + + {% for name,title in page_types %} + + + + + {% endfor %} +
    + + {% set module_types = admin.modularTypes(null) %} {# directory.config('filters.ignore_module_types') #} + {% if module_types %} +
    + + {{ 'PLUGIN_FLEX_OBJECTS.FILTER.MODULAR_TYPES'|tu }} + + {% for name,title in module_types %} + + + + + {% endfor %} +
    + {% endif %} + + + {{ 'PLUGIN_FLEX_OBJECTS.ACTION.APPLY_FILTERS'|tu }} + + + {{ 'PLUGIN_FLEX_OBJECTS.ACTION.RESET_FILTERS'|tu }} + +
    +
    +
    + +
    +
    {{ 'PLUGIN_FLEX_OBJECTS.STATE.LOADING'|tu }}
    +
    +
    + +
    + + {# Modals #} +
    + {% include 'partials/blueprints-new.html.twig' with { blueprints: admin.blueprints('admin/pages/new'), data: obj_data, form_id: 'new-page' } %} +
    + +
    + {% include 'partials/blueprints-new-folder.html.twig' with { blueprints: admin.blueprints('admin/pages/new_folder'), data: obj_data, form_id: 'new-folder' } %} +
    + +
    + {% include 'partials/blueprints-new.html.twig' with { blueprints: admin.blueprints('admin/pages/modular_new'), data: obj_data, form_id: 'new-module' } %} +
    + + {% for key, add_modal in config.plugins.admin.add_modals %} +
    + {% include add_modal.template|defined('partials/blueprints-new.html.twig') with { + blueprints: admin.blueprints(add_modal.blueprint), + data: obj_data, + form_id: 'add-modal' + }|merge(add_modal.with|defined({})) %} +
    + {% endfor %} + +
    + {% include 'partials/blueprints-copy.html.twig' with { blueprints: admin.blueprints('admin/pages/copy'), data: obj_data, form_id: 'copy' } %} +
    + +
    +
    +

    Parents

    +
    +
    {{ 'PLUGIN_FLEX_OBJECTS.STATE.LOADING'|tu }}
    +
    +
    + +
    +
    + +
    +
    +

    {{ "PLUGIN_ADMIN.MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE"|tu }}

    +

    + {% if context %} + {{ "PLUGIN_ADMIN.PAGE"|tu }}: {{ context.title }} + {% endif %} +

    +

    + {{ "PLUGIN_ADMIN.MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_DESC"|tu }} +

    +
    +
    + + {{ "PLUGIN_ADMIN.CONTINUE"|tu }} +
    +
    +
    +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/list/list.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/list/list.html.twig new file mode 100644 index 0000000..12ac6ad --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/list/list.html.twig @@ -0,0 +1,41 @@ +{% extends 'flex-objects/types/default/list/list.html.twig' %} + +{% block modals %} +
    + {% include 'partials/blueprints-new.html.twig' with { blueprints: admin.blueprints('admin/pages/new'), data: obj_data, form_id: 'new-page' } %} +
    + +
    + {% include 'partials/blueprints-new-folder.html.twig' with { blueprints: admin.blueprints('admin/pages/new_folder'), data: obj_data, form_id: 'new-folder' } %} +
    + +
    + {% include 'partials/blueprints-new.html.twig' with { blueprints: admin.blueprints('admin/pages/modular_new'), data: obj_data, form_id: 'new-module' } %} +
    + + {% for key, add_modal in config.plugins.admin.add_modals %} +
    + {% include add_modal.template|defined('partials/blueprints-new.html.twig') with { + blueprints: admin.blueprints(add_modal.blueprint), + data: obj_data, + form_id: 'add-modal' + }|merge(add_modal.with|defined({})) %} +
    + {% endfor %} + +
    + {% include 'partials/blueprints-copy.html.twig' with { blueprints: admin.blueprints('admin/pages/copy'), data: obj_data, form_id: 'copy' } %} +
    + +
    +
    +

    Parents

    +
    {{ 'PLUGIN_FLEX_OBJECTS.STATE.LOADING'|tu }}
    +
    + +
    +
    +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/preview.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/preview.html.twig new file mode 100644 index 0000000..61499ee --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/pages/preview.html.twig @@ -0,0 +1,16 @@ +{% extends 'flex-objects/types/default/preview.html.twig' %} + +{% set can_translate = can_translate ?? (admin.multilang and object.hasFlexFeature('page-translate')) %} + +{% block back_button %} + {% include ['flex-objects/types/' ~ target ~ '/buttons/back.html.twig', 'flex-objects/types/pages/buttons/back.html.twig'] + with { back_url: back_url } %} +{% endblock back_button %} + +{% block body %} + {% set parent = object.parent %} + + {% set preview_url = preview_url ?: (object.home ? '/' : '') %} + + {{ parent() }} +{% endblock body %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/configure.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/configure.html.twig new file mode 100644 index 0000000..2348417 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/configure.html.twig @@ -0,0 +1,9 @@ +{% extends 'flex-objects/types/default/configure.html.twig' %} + +{% set back_route = back_route ?? ('/' ~ route.getRoute(1, -1)) %} + +{% block content_top %} + {% include 'flex-objects/layouts/accounts/partials/top.html.twig' %} + + {{ parent() }} +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/edit.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/edit.html.twig new file mode 100644 index 0000000..85fe5fd --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/edit.html.twig @@ -0,0 +1,9 @@ +{% extends 'flex-objects/types/default/edit.html.twig' %} + +{% if not directory.isAuthorized('list', 'admin', user) %} + {% set back_route = '/' %} +{% endif %} + +{% if not object.exists %} + {% do object.onPrepareRegistration() %} +{% endif %} \ No newline at end of file diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/list.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/list.html.twig new file mode 100644 index 0000000..0cefbb8 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/user-accounts/list.html.twig @@ -0,0 +1,7 @@ +{% extends 'flex-objects/types/default/list.html.twig' %} + +{% block content_top %} + {% include 'flex-objects/layouts/accounts/partials/top.html.twig' %} + + {{ parent() }} +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/configure.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/configure.html.twig new file mode 100644 index 0000000..2348417 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/configure.html.twig @@ -0,0 +1,9 @@ +{% extends 'flex-objects/types/default/configure.html.twig' %} + +{% set back_route = back_route ?? ('/' ~ route.getRoute(1, -1)) %} + +{% block content_top %} + {% include 'flex-objects/layouts/accounts/partials/top.html.twig' %} + + {{ parent() }} +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/list.html.twig b/user/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/list.html.twig new file mode 100644 index 0000000..0cefbb8 --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/flex-objects/types/user-groups/list.html.twig @@ -0,0 +1,7 @@ +{% extends 'flex-objects/types/default/list.html.twig' %} + +{% block content_top %} + {% include 'flex-objects/layouts/accounts/partials/top.html.twig' %} + + {{ parent() }} +{% endblock %} diff --git a/user/plugins/flex-objects/admin/templates/forms/fields/flex-objects/flex-objects.html.twig b/user/plugins/flex-objects/admin/templates/forms/fields/flex-objects/flex-objects.html.twig new file mode 100644 index 0000000..92db6dc --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/forms/fields/flex-objects/flex-objects.html.twig @@ -0,0 +1,69 @@ +{% extends "forms/field.html.twig" %} + +{% macro spanToggle(input, length) %} + {% set space = repeat('  ', (length - input|length) / 2) %} + {{ (space ~ input ~ space)|raw }} +{% endmacro %} + +{% import _self as macro %} + +{% set value = (value is null ? field.default : value) %} + +{% block global_attributes %} + {{ parent() }} + data-grav-field-name="{{ (scope ~ field.name)|fieldName }}" +{% endblock %} + +{% block input %} + {% set flex = grav['flex_objects'] %} + {% set all = flex.blueprints %} + {% if all|count %} + {% set legacy = flex.getLegacyBlueprintMap() %} + {% for label, directory in all %} + {% set url = directory.blueprintFile %} + {% set found = url in value %} + {% if not found and legacy[url] is defined %} + {% set found = legacy[url] in value %} + {% endif %} + +
    +
    + {% set maxLen = 0 %} + {% for text in ['PLUGIN_ADMIN.ENABLED', 'PLUGIN_ADMIN.DISABLED'] %} + {% set translation = grav.twig.twig.filters['tu'] is defined ? text|tu : text|t %} + {% set maxLen = max(translation|length, maxLen) %} + {% endfor %} + + {% set id = "toggle_" ~ field.name ~ '_' ~ label %} + + + {% set text = 'PLUGIN_ADMIN.ENABLED' %} + {% set translation = (grav.twig.twig.filters['tu'] is defined ? text|tu : text|t)|trim %} + + + {% set text = 'PLUGIN_ADMIN.DISABLED' %} + {% set translation = (grav.twig.twig.filters['tu'] is defined ? text|tu : text|t)|trim %} + +
    + {{ directory.title }} +
    + {% endfor %} + {% else %} +
    {{ 'PLUGIN_FLEX_OBJECTS.ERROR.NO_FLEX_DIRECTORIES'|tu }}
    + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/user/plugins/flex-objects/admin/templates/forms/fields/save-redirect/save-redirect.html.twig b/user/plugins/flex-objects/admin/templates/forms/fields/save-redirect/save-redirect.html.twig new file mode 100644 index 0000000..2541a3f --- /dev/null +++ b/user/plugins/flex-objects/admin/templates/forms/fields/save-redirect/save-redirect.html.twig @@ -0,0 +1,37 @@ +{% extends "forms/field.html.twig" %} + +{% set originalValue = value %} +{% set value = (value is null ? field.default : value) %} +{% set isNew = key ? false : true %} +{% set savedOption = grav.session.post_entries_save|default('create-new') %} + +{% if isNew %} + {% set options = {'create-new':'PLUGIN_FLEX_OBJECTS.ACTION.CREATE_NEW', 'edit':'PLUGIN_FLEX_OBJECTS.ACTION.EDIT_ITEM', 'list':'PLUGIN_FLEX_OBJECTS.ACTION.LIST_ITEMS'} %} +{% else %} + {% set options = {'edit':'PLUGIN_FLEX_OBJECTS.ACTION.EDIT_ITEM', 'list':'PLUGIN_FLEX_OBJECTS.ACTION.LIST_ITEMS'} %} +{% endif %} + +{% block input %} + {% set savedOption = not isNew and savedOption == 'create-new' ? 'edit' : savedOption %} + {% for key, text in options %} + {% set id = field.id|default(field.name) ~ '-' ~ key %} + + {% if savedOption == key %} + {% set value = savedOption %} + {% endif %} + + + + + + + + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/user/plugins/flex-objects/app/columns/finder.js b/user/plugins/flex-objects/app/columns/finder.js new file mode 100644 index 0000000..ac623b2 --- /dev/null +++ b/user/plugins/flex-objects/app/columns/finder.js @@ -0,0 +1,425 @@ +import $ from 'jquery'; +import Finder from '../utils/finder'; +import { getInitialRoute, getStore, setInitialRoute } from './index'; +// import getFilters from '../utils/get-filters'; + +let XHRUUID = 0; +const GRAV_CONFIG = typeof global.GravConfig !== 'undefined' ? global.GravConfig : global.GravAdmin.config; + +export const Instances = {}; + +const isInViewport = (elem) => { + const bounding = elem.getBoundingClientRect(); + const titlebar = document.querySelector('#titlebar'); + const offset = titlebar ? titlebar.getBoundingClientRect().height : 0; + return ( + bounding.top >= offset && + bounding.left >= 0 && + bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + bounding.right <= (window.innerWidth || document.documentElement.clientWidth) + ); +}; + +export class FlexPages { + constructor(container, data) { + this.container = $(container); + this.data = data; + const dataLoad = this.dataLoad; + + this.finder = new Finder( + this.container, + (parent, callback) => { + return dataLoad.call(this, parent, callback); + }, + { + labelKey: 'title', + defaultPath: getInitialRoute(), + itemTrigger: '[data-flexpages-expand]', + createItem: function(item) { + return FlexPages.createItem(this.config, item, this); + }, + createItemContent: function(item) { + return FlexPages.createItemContent(this.config, item, this); + } + } + ); + + this.finder.$emitter.on('leaf-selected', (item) => { + setInitialRoute({ + route: item.route.raw + }); + }); + + this.finder.$emitter.on('interior-selected', (item) => { + setInitialRoute({ + route: item.route.raw + }); + }); + + /* + this.finder.$emitter.on('leaf-selected', (item) => { + console.log('selected', item); + this.finder.emit('create-column', () => this.createSimpleColumn(item)); + }); + + this.finder.$emitter.on('item-selected', (selected) => { + console.log('selected', selected); + // for future use only - create column-card creation for file with details like in macOS finder + // this.finder.$emitter('create-column', () => this.createSimpleColumn(selected)); + }); */ + + this.finder.$emitter.on('column-created', () => { + this.container[0].scrollLeft = this.container[0].scrollWidth - this.container[0].clientWidth; + }); + } + + static createItem(config, item, finder) { + const listItem = $('
  • '); + const listItemClasses = [config.className.item]; + // const href = `${GRAV_CONFIG.current_url}/${item.route.raw}`.replace('//', '/'); + const link = $('
    '); + const createItemContent = config.createItemContent || finder.createItemContent; + const fragment = createItemContent.call(this, item); + link.append(fragment) + // .attr('href', href) + .attr('tabindex', -1); + + if (item.url) { + link.attr('href', item.url); + listItemClasses.push(item.className); + } + + if (item[config.childKey]) { + listItemClasses.push(config.className[config.childKey]); + } + + if (item.filters_hit) { + listItemClasses.push('filters-hit'); + } + + listItem.addClass(listItemClasses.join(' ')); + listItem.append(link) + .attr('data-fjs-item', item[config.itemKey]); + + listItem[0]._item = item; + + return listItem; + } + + static createItemContent(config, item) { + const frag = document.createDocumentFragment(); + const route = `${GRAV_CONFIG.current_url}/${item.route.raw}`.replace('//', '/'); + const title = $('
    '); + const link = $(``); + const icon = $(``); + + if (item.extras && item.extras.lang) { + let status = ''; + if (item.extras.translated) { + status = 'translated'; + } + + if (item.extras.lang === 'n/a') { + status = 'not-available'; + } + + const lang = $(`${item.extras.lang}`); + lang.appendTo(icon); + } + + if (item.extras && item.extras && (item.extras.published_date || item.extras.unpublished_date)) { + const clock = $(''); + clock.appendTo(icon); + } + + const info = $(`${item.title} ${item.route.display}`); + const actions = $(''); + + let dotdotdot = null; + if (item.extras) { + const LANG_URL = $('[data-lang-url]').data('langUrl'); + dotdotdot = $('
    '); + dotdotdot.on('click', (event) => { + if (!dotdotdot.find('.dropdown-menu').length) { + let tags = ''; + let langs = ''; + + item.extras.tags.forEach((tag) => { + tags += `${tag}`; + }); + + const translations = item.extras.langs || {}; + Object.keys(translations).forEach((lang) => { + const translated = translations[lang]; + langs += `
    ${lang ? lang : 'default'}`; + }); + + const canPreview = item.extras.actions.includes('preview') && (!(item.extras.tags.includes('non-routable') || item.extras.tags.includes('unpublished'))); + const canEdit = item.extras.actions.includes('edit'); + const canCopy = item.extras.actions.includes('copy'); + const canMove = false; // item.extras.actions.includes('move'); + const canDelete = item.extras.actions.includes('delete'); + const ul = $(``); + ul.appendTo(dotdotdot); + } + + return true; + }); + } + + if (item.child_count) { + const button = $('