1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-28 08:59:52 +02:00

403 Commits
3.0.227 ... dev

Author SHA1 Message Date
Ryan Cramer
8ee607f06f Update InputfieldRepeater so that it also supports custom repeater item colors in Konkat default theme, plus some other adjustments 2025-08-22 15:14:49 -04:00
Ryan Cramer
0b4262b57b Phpdoc adjustments, improvements, additions and corrections to around 49 modules to improve output in the ProcessWire API reference 2025-08-22 13:21:16 -04:00
Ryan Cramer
4cb2061925 Minor adjustments to Konkat default theme admin css 2025-08-22 13:05:38 -04:00
Ryan Cramer
bf22960d11 Minor adjustments to AdminThemeUikit Konkat default theme 2025-08-08 16:38:23 -04:00
Ryan Cramer
d6837c8e5c Add support for HTML5 autocomplete attributes to InputfieldText, when used of page editing forms 2025-08-03 13:38:40 -04:00
Ryan Cramer
c923a1b568 Fix issue in FieldtypeMulti where $page->field_name("limit=0") would return a pagination rather than ALL 2025-08-03 12:19:07 -04:00
Ryan Cramer
03e7b13fce Fix duplicate entity encode in InputfieldDatetimeSelect 2025-08-01 16:37:52 -04:00
Ryan Cramer
db0d1321c8 Minor updates to SessionLoginThrottle 2025-08-01 16:37:06 -04:00
Ryan Cramer
e95661c439 Updates to AdminThemeUikit Konkat default theme 2025-08-01 16:36:40 -04:00
Ryan Cramer
dc89e16131 Update WireShutdown to support sanitization of debug backtraces 2025-08-01 16:34:57 -04:00
Ryan Cramer
c1fb7264c8 Minor phpdoc updates for API reference 2025-08-01 16:34:26 -04:00
Ryan Cramer
4acfc353cd Bump version to 3.0.251 2025-07-25 16:45:52 -04:00
Ryan Cramer
abab9bf4a8 Update to use versioned asset URLs in AdminThemeUikit Konkat default theme 2025-07-24 15:46:24 -04:00
Ryan Cramer
8b39c92b31 Update AdminThemeUikit Konkat default theme to use --pw- namespaced CSS variables.
Co-authored-by: ocorreiododiogo <ocorreiododiogo@gmail.com>
Co-authored-by: jploch <jploch@users.noreply.github.com>
2025-07-23 15:22:16 -04:00
Ryan Cramer
7d5ad4b277 Add new string() method to WireRandom class. This method lets you specify that it should create a random string using the given characters. 2025-07-23 15:11:25 -04:00
Ryan Cramer
27f20b4799 Additional update to DatabaseQuerySelect to force non-match for search words consisting only of operator characters 2025-07-20 11:26:20 -04:00
Ryan Cramer
ff69aa56a1 Update DatabaseQuerySelectFulltext so that too-long words combined with individual word-matching operators are truncated rather than removed. 2025-07-20 10:29:27 -04:00
Ryan Cramer
eda5129884 Update $sanitizer->wordsArray() to support a truncate option 2025-07-20 10:28:43 -04:00
Ryan Cramer
5fb798857f Additional file meant to be included in last commit 2025-07-18 17:04:40 -04:00
Ryan Cramer
ece4917b1b Add support for Process modules to optionally use icons in their headlines, by specifying <icon-name> in the headline (where 'name' is an FA icon name). 2025-07-18 17:03:12 -04:00
Ryan Cramer
e6160973e0 Fix issue in ProcessPageLister where subfields could be incorrectly linked show page actions when clicked on, but only if the first column showed a subfield from the same field 2025-07-18 16:30:52 -04:00
Ryan Cramer
0da85dd55c Updates to TinyMCE content.css styles, primarily to make more readable code and pre element styles 2025-07-18 16:30:13 -04:00
Ryan Cramer
95cca5ad62 Add new AdminThemeUikit CSS toggles for bolder repeater item headers, input focused states, and page list buttons 2025-07-18 16:28:44 -04:00
Ryan Cramer
a817943b73 Additional AdminThemeUikit CSS updates
Co-authored-by: ocorreiododiogo <ocorreiododiogo@gmail.com>
Co-authored-by: jploch <jploch@users.noreply.github.com>>
2025-07-18 16:24:47 -04:00
Ryan Cramer
96165bbff4 Add newest Konkat admin.css and admin-custom.css for AdminThemeUikit
Co-authored-by: ocorreiododiogo <ocorreiododiogo@gmail.com>
Co-authored-by: jploch <jploch@users.noreply.github.com>>
2025-07-18 10:20:45 -04:00
Ryan Cramer
f03498e88e Bump version to 3.0.250 2025-07-11 16:55:27 -04:00
Ryan Cramer
d11a1e631b Updates and new features to the Markup Regions system 2025-07-11 15:36:21 -04:00
Ryan Cramer
3770a8f74b Fix issue processwire/processwire-issues#2074 2025-07-01 10:45:29 -04:00
Ryan Cramer
f4487628ad Fix issue processwire/processwire-issues#2085 2025-07-01 10:32:51 -04:00
Ryan Cramer
f15c5942b4 Fix issue processwire/processwire-issues#2091 2025-07-01 10:29:09 -04:00
Ryan Cramer
e526d6562d Fix issue processwire/processwire-issues#2101 2025-07-01 10:08:08 -04:00
Ryan Cramer
b579e24425 Fix extra ‘for’ attribute issue in InputfieldWrapper (that had value ‘{for}’), plus add new removeByName() method for removing an Inputfield from the form by name. 2025-07-01 09:40:01 -04:00
Ryan Cramer
1b0d51e275 Add AdminThemeUikit feature for testing all Uikit features with current theme (whether original, admin.less based, or themes/name/admin.css based). This is adapted from the /tests/ directory included with Uikit. 2025-06-18 14:11:58 -04:00
Ryan Cramer
36980a2455 Bump version to 3.0.249 2025-06-13 15:46:05 -04:00
Ryan Cramer
5373613169 Fix minor issue in InputfieldTinyMCE.js 2025-06-13 14:00:52 -04:00
Ryan Cramer
c9650f1974 Update PagesType class (used by $users, $roles, $permissions API vars) to have a new() method that creates a new Page instance (of type User, Permission, Role) in memory. 2025-06-13 13:59:39 -04:00
Ryan Cramer
9f9cc65680 Update $pages->newPage() method so that it can also accept template name or ID for the template option 2025-06-13 13:57:36 -04:00
Ryan Cramer
99cdf49bad Update installer to generate random session cookie name. This helps if you have multiple installs, switching from one to the other won't cause logout 2025-06-13 13:55:20 -04:00
Ryan Cramer
24ac500048 Minor updates 2025-06-06 16:15:24 -04:00
Ryan Cramer
eb48adccb2 Various small updates to AdminThemeUikit default theme. In addition, module authors can now diallow dark mode with $config->AdminThemeUikit('noDarkMode', true); and they can disable toggle style checkboxes with $config->AdminThemeUikit('noTogcbx', true); 2025-06-04 10:04:11 -04:00
Ryan Cramer
c1259148d2 Update InputfieldTinyMCE to add an 'InputfieldTinyMCEFocused' class to .Inputfield element when the TinyMCE input is focused 2025-05-30 15:01:28 -04:00
Ryan Cramer
6566c5795c AdminThemeUikit default theme: Add various CSS updates from Diogo, plus add an option to disable toggle-style checkboxes for those that prefer regular checkboxes.
Co-authored-by: ocorreiododiogo <ocorreiododiogo@gmail.com>
2025-05-30 14:59:35 -04:00
Ryan Cramer
44e6c89e35 A few small updates to default AdminThemeUikit theme module config 2025-05-23 16:35:32 -04:00
Ryan Cramer
050fb0aa0c Add AdminThemeUikit default theme with attribution on-behalf-of: @konkat-studio <hi@konkat.studio>
Co-authored-by: ocorreiododiogo <ocorreiododiogo@gmail.com>
Co-authored-by: jploch <jploch@users.noreply.github.com>
2025-05-23 10:12:45 -04:00
Ryan Cramer
b066de77b6 Remove default to add back with correct author in next commits 2025-05-23 10:00:42 -04:00
Ryan Cramer
731578fbed Admin CSS update from Diogo that adds a variable for border radius for buttons 2025-05-23 09:59:06 -04:00
Ryan Cramer
8248143aee Set installer to use light theme 2025-05-23 09:58:30 -04:00
Ryan Cramer
9fb83ffe66 Update AdminThemeUikit new design to support separate light and dark mode main colors. Add support for embedded/inline SVG logos when they support fill="currentColor". 2025-05-16 16:26:07 -04:00
Ryan Cramer
f627e68a6f Minor correction in WireArray 2025-05-16 16:05:23 -04:00
Ryan Cramer
6a48a29b2d Fix a php header() call 2025-05-12 11:31:40 -04:00
Ryan Cramer
bc0faa771f Small adjustments 2025-05-09 16:09:52 -04:00
Ryan Cramer
b786dcff14 Bump version to 3.0.248, adding new admin resign by KONKAT Studio @konkat-studio 2025-05-09 13:38:50 -04:00
Ryan Cramer
e0f96733e6 Minor updates related to the add-new links in the dropdown nav page-tree 2025-05-07 12:41:37 -04:00
Ryan Cramer
77fbde3409 Accidentally left a Tracy bd() call in there, this commit removes it 2025-05-02 15:13:30 -04:00
Ryan Cramer
fb26ec9d64 Fix issue processwire/processwire-issues#2066 2025-05-02 11:57:18 -04:00
Ryan Cramer
e8f8317b48 Fix issue processwire/processwire-issues#2067 2025-05-02 11:46:44 -04:00
Ryan Cramer
8322725f35 Make InputfieldTinyMCE::getModuleConfigInputfields() method hookable 2025-05-02 09:59:56 -04:00
Ryan Cramer
c1d150d4ba Minor TinyMCE updates plus make a method hookable for more configuration options 2025-05-01 13:38:17 -04:00
Ryan Cramer
b5f94df3d6 Updates to AdminThemeUikit in preparation for updated design 2025-05-01 13:35:10 -04:00
Ryan Cramer
a3dbd1b28f Prepare AdminThemeUikit for new design theme options 2025-04-25 14:29:52 -04:00
Ryan Cramer
69823256fb Update ProcessWire.alert(), ProcessWire.confirm() and ProcessWire.prompt() Javascript functions to use Uikit modals rather than Vex modals. This also should also help to solve processwire/processwire-issues#2063 ...also added ProcessWire.entities1() and ProcessWire.unentities() JS functions to mirror those in Sanitizer. 2025-04-18 13:09:31 -04:00
Ryan Cramer
436a0ed3b1 Fix issue processwire/processwire-issues#2064 by adding support for exception 'code' string values, like those used by PDOException, and added new WireException() function to faciliate it. 2025-04-18 10:37:29 -04:00
Ryan Cramer
5f17f5ff9d Phpdoc update in InputfieldWrapper for ProcessWireAPI docs 2025-04-17 12:14:26 -04:00
Ryan Cramer
0a317971f8 Phpdoc updates in Page.php 2025-04-17 12:13:47 -04:00
Ryan Cramer
b4ddc89fb9 Add support in MarkupRegions for removing class names by wildcard. For instance, <div id="content" class="-uk-width-*" pw-append></div> would remove all class names from #content div starting with "uk-width-". Regex patterns are also supported when pattern is identifed by /, i.e. <div id="content" class="-/^uk-(text|width).+$/" pw-append></div> would remove all class names from $content starting with "uk-text" or "uk-width". 2025-04-17 12:03:53 -04:00
Ryan Cramer
570bdb4b81 Fix issue processwire/processwire-issues#2060 2025-04-17 11:59:47 -04:00
Ryan Cramer
725d89e664 Fix issue processwire/processwire-issues#2053 2025-03-28 14:46:17 -04:00
Ryan Cramer
2715611949 Updates for ProcessLogin and Inputfield::skipLabelHidden option 2025-03-28 14:10:24 -04:00
Ryan Cramer
ee96262e44 Fix issue processwire/processwire-issues#2056 2025-03-28 13:22:34 -04:00
Ryan Cramer
dae4e59db9 Fix issue processwire/processwire-issues#2057 2025-03-28 13:21:05 -04:00
Ryan Cramer
bc8a1959f3 Fix issue processwire/processwire-issues#2058 2025-03-28 13:05:54 -04:00
Ryan Cramer
6783c4824b Fix function call error in PagesVersions module hasPageVersions method 2025-03-16 10:00:40 -04:00
Ryan Cramer
4725ece5f8 Fix ProcessPageLister issue when a custom page class is expected for a page without an ID 2025-03-07 14:36:11 -05:00
Ryan Cramer
422d9c1eb6 Adjustment to ProcessLogin 2025-03-07 14:35:46 -05:00
Ryan Cramer
a17ebff0cd Normalize background for file/image Inputfields in AdminThemeUikit 2025-03-07 14:32:38 -05:00
Ryan Cramer
afacf91792 Fix issue processwire/processwire-issues#2051 2025-02-28 14:27:41 -05:00
Ryan Cramer
0c3c15c29d Fix issue processwire/processwire-issues#2052 2025-02-28 14:00:45 -05:00
Ryan Cramer
b7636bc1df I seem to be bad at spelling the word langauges oops languages 2025-02-28 13:53:03 -05:00
Ryan Cramer
a8ea954462 Bump version to 3.0.247 2025-02-14 15:00:04 -05:00
Ryan Cramer
8ee238ea70 Fix issue processwire/processwire-issues#2045 2025-02-14 14:58:18 -05:00
Ryan Cramer
668081fb23 Fix issue processwire/processwire-issues#2041 2025-02-14 14:43:37 -05:00
Ryan Cramer
9996091afb Add support for conditional hooks that match method return values. For details see: https://processwire.com/docs/modules/hooks/#conditional-hooks-matching-return-value-or-type 2025-02-14 13:13:51 -05:00
Ryan Cramer
44fcf13ea2 Bump version to 3.0.246 2025-02-14 10:10:03 -05:00
Ryan Cramer
69270a31b0 Fix issue processwire/processwire-issues#2039 2025-02-14 10:00:01 -05:00
Ryan Cramer
f88350baa5 Fix issue processwire/processwire-issues#2040 2025-02-14 09:57:42 -05:00
Ryan Cramer
c7ba08ecb9 Update ProcessController::jsonMessage() method so that it supports array argument, primarily for internal debugging purposes 2025-02-07 14:21:58 -05:00
Ryan Cramer
59ae7f4c7f Fix issue processwire/processwire-issues#2023 2025-02-06 11:04:21 -05:00
Ryan Cramer
0708865081 Update min PHP version in composer.json file. processwire/processwire-issues#2024 2025-02-06 10:50:17 -05:00
Ryan Cramer
6ae349b4ec Fix issue processwire/processwire-issues#2024 2025-02-06 10:35:22 -05:00
Ryan Cramer
30b34c70b3 Fix issue processwire/processwire-issues#2025 2025-02-06 10:31:22 -05:00
Ryan Cramer
e6dbc3e8eb Fix issue processwire/processwire-issues#2026 2025-02-06 10:06:10 -05:00
Ryan Cramer
a959afc422 Fix issue processwire/processwire-issues#2029 2025-02-06 09:04:08 -05:00
Ryan Cramer
4a9b904b77 Fix issue processwire/processwire-issues#2030 2025-02-06 08:48:36 -05:00
Ryan Cramer
8b5d96f1b6 Fix issue processwire/processwire-issues#2035 plus some related additions to help identify and fix pages that might incorrectly have trash status 2025-02-05 14:45:05 -05:00
Ryan Cramer
eddd6cb8ad Bump version to 3.0.245 2025-01-29 15:22:03 -05:00
Ryan Cramer
e1e938591d Updates to ProcessPageList to make some parts more hookable 2025-01-29 10:02:06 -05:00
Ryan Cramer
1805ad0a59 Improvements to FieldtypePage set of string value(s) to pages, so that it recognizes title and name for setting purposes. 2025-01-29 09:55:22 -05:00
Ryan Cramer
f22739a54c Bump version to 3.0.244 2025-01-10 14:13:34 -05:00
Ryan Cramer
4e678c1584 Various minor updates 2025-01-10 10:27:09 -05:00
Ryan Cramer
4604c09abc More updates for processwire/processwire-issues#2015 2025-01-09 11:44:20 -05:00
Ryan Cramer
be3d17b9c2 Fix issue processwire/processwire-issues#2022 2025-01-09 10:20:07 -05:00
Ryan Cramer
54e75701c1 Fix issue processwire/processwire-issues#2021 2025-01-09 10:09:20 -05:00
Ryan Cramer
29b1fa0e45 Update installer with changes similar to PR #300 to avoid PHP session warnings in some environments
Co-authored-by: poljpocket <mail@poljpocket.com>
2025-01-08 10:50:47 -05:00
Ryan Cramer
870284072c Some more improvements to UTF-8 page names, also related to processwire/processwire-issues#2015 2025-01-08 10:09:39 -05:00
Ryan Cramer
552fd7180e Fix issue processwire/processwire-issues#1950 2025-01-07 10:42:38 -05:00
Ryan Cramer
9db14e6aef Fix issue processwire/processwire-issues#1915 using fix suggested from @michaellenaghan 2025-01-05 11:57:02 -05:00
matjazpotocnik
8d2ad63ce7 Fix issue processwire/processwire-issues#1916 2025-01-05 11:48:54 -05:00
Ryan Cramer
f1819b5cd8 Fix issue processwire/processwire-issues#1914 2025-01-05 11:40:49 -05:00
Ryan Cramer
807e94e22a Installer updates, make utf8mb4 and InnoDB the default settings for MySQL (rather than utf8 and MyISAM) 2025-01-05 11:21:52 -05:00
Ryan Cramer
bd5200dfb2 Fix issue processwire/processwire-issues#1966 2025-01-05 10:54:17 -05:00
Ryan Cramer
00a6baaac9 Update for processwire/processwire-issues#2015 2025-01-05 10:11:20 -05:00
Ryan Cramer
e6ace73c02 Additional updates for processwire/processwire-issues#2015 2025-01-03 12:43:17 -05:00
Ryan Cramer
4be389067d Fix issue processwire/processwire-issues#2015 2024-12-31 10:59:47 -05:00
Ryan Cramer
fa47338eed Fix ProcessPageList label issue reported in ryancramerdesign/PageListCustomChildren#1 2024-12-27 12:54:15 -05:00
Ryan Cramer
cef47391ee Fix issue processwire/processwire-issues#1875 2024-12-27 12:44:02 -05:00
Ryan Cramer
86fc754ffb Add collapsed options back for InputfieldWrapper/InputfieldFieldset per processwire/processwire-issues#1953 2024-12-27 12:17:25 -05:00
Ryan Cramer
6036118b15 Fix phpdoc example for MarkupHTMLPurifier processwire/processwire-issues#226 2024-12-23 11:57:30 -05:00
Ryan Cramer
16d70048c1 Fix issue processwire/processwire-issues#1934 2024-12-23 11:52:06 -05:00
Ryan Cramer
1f7d039b3e Fix issue processwire/processwire-issues#1927 2024-12-23 11:35:34 -05:00
Ryan Cramer
94bc7c346e Bump version to 3.0.243 2024-12-20 16:03:28 -05:00
Ryan Cramer
b7238605e4 Minor code optimizations to the PagesExportImport modules 2024-12-20 15:15:51 -05:00
Ryan Cramer
1fc3cf414a Fix issue where the config.maxUrlSegments wasn't working, plus add new config.longUrlResponse where you can define the http response that should be used when there is an overflow of URL length, segments, or depth. 2024-12-20 15:14:37 -05:00
Ryan Cramer
9bc02399e5 Fix issue processwire/processwire-issues#2007 2024-12-13 10:20:32 -05:00
Ryan Cramer
68fa2b47f6 Fix issue processwire/processwire-issues#2005 2024-12-13 09:10:37 -05:00
Ryan Cramer
2361b90739 Fix issue processwire/processwire-issues#2002 2024-11-29 13:43:26 -05:00
Ryan Cramer
1c5f2f7e3c Updates for PHP 8.4 support per processwire/processwire-issues#2000 Also note that these changes require we adjust our minimum required PHP version from 7.0 up to 7.1, with PHP 8.x still recommended. 2024-11-29 13:29:28 -05:00
Ryan Cramer
ac4dfebfab Update Fields class to keep an index of Field flags that can be read before fields are loaded. Also added a findByFlag() method that uses the index. 2024-11-29 13:00:09 -05:00
Ryan Cramer
405da182d5 Fix issue processwire/processwire-issues#2004 2024-11-29 11:08:08 -05:00
Ryan Cramer
0ea71c3e1d Fix issue processwire/processwire-issues#1097 2024-11-22 15:10:22 -05:00
Ryan Cramer
ede080e2a8 Fix issue processwire/processwire-issues#1962 2024-11-22 14:37:00 -05:00
Ryan Cramer
496509c39f Fix issue processwire/processwire-issues#1960 2024-11-22 14:22:52 -05:00
Ryan Cramer
7b893abba3 Add suggested fix for processwire/processwire-issues#1975 2024-11-22 14:12:32 -05:00
Ryan Cramer
cf0abe538a Additional updates for processwire/processwire-issues#1467 2024-11-22 10:57:01 -05:00
Ryan Cramer
3bd27723b2 Fix issue processwire/processwire-issues#1999 2024-11-22 10:44:30 -05:00
Ryan Cramer
1a5760a5e8 Fix issue processwire/processwire-issues#1997 2024-11-22 09:42:13 -05:00
Ryan Cramer
5ca977f6df Fix issue processwire/processwire-issues#1996 2024-11-22 09:37:23 -05:00
Ryan Cramer
57388db576 Fix issue processwire/processwire-issues#1993 2024-11-22 09:32:33 -05:00
Ryan Cramer
fb641fae89 Change default of ProcessModule 'allowClasses' option to true so that it shows class names below module titles. 2024-11-15 18:19:31 -05:00
Ryan Cramer
a14398b4a3 Update phpdoc related to return value and options for the page preload methods 2024-11-15 15:37:22 -05:00
Ryan Cramer
8a1ba87298 Update to the viewable permission check in ProcessPageEditLink module 2024-11-15 14:33:24 -05:00
Ryan Cramer
53b7aa39eb Update PagesLoader.preloadPage() method to support a loadPageRefs option which loads page references separately and as a group. Not yet certain this will remain though, as it seems like the FieldtypePage loader is already fast enough 2024-11-15 14:31:03 -05:00
Ryan Cramer
ca74514288 Update InputfieldSelector to support a "children…" selection that works similarly to the "parent…" selection 2024-11-15 14:30:08 -05:00
Ryan Cramer
06ac399319 Fix on the modules install screen when entering a module name with trailing whitespace so that it doesn't get converted to underscore and result in unknown module message 2024-11-15 14:28:44 -05:00
Ryan Cramer
b374ed83e2 Minor phpdoc update in Template class 2024-11-15 14:28:08 -05:00
Ryan Cramer
6c8ca289ba Add new $pages->cache()->hasCache($page_id) method 2024-11-15 14:27:30 -05:00
Ryan Cramer
ec8943c26d Fix option order issue in $pages->getByIDs() method 2024-11-15 14:26:40 -05:00
Ryan Cramer
5a8732f1e1 Update InputfieldWrapper for Inputfield::skipLabelFor property to fix processwire/processwire-issues#1982 2024-11-15 12:41:10 -05:00
Ryan Cramer
1191c164a2 Fix issue processwire/processwire-issues#1988 2024-11-14 14:26:37 -05:00
Ryan Cramer
d84d40e84c Attempt fix issue processwire/processwire-issues#1992 2024-11-14 14:12:09 -05:00
Ryan Cramer
5481d713ab Add new $page->preload([ 'field1', 'field2', 'etc.' ]); method that preloads multiple fields in a single query. This is kind of like autojoin except that it can be called on an already-loaded page. 2024-11-08 15:23:37 -05:00
Ryan Cramer
af5cbd7e3c Bump version to 3.0.242 2024-11-01 14:59:38 -04:00
Ryan Cramer
4d6589bdc8 Fix issue processwire/processwire-issues#1980 2024-11-01 10:57:53 -04:00
Ryan Cramer
b2b810f181 Fix issue processwire/processwire-issues#1984 2024-11-01 10:27:29 -04:00
Ryan Cramer
5e91b745e1 Add hookable method in ProcessPageSearchLive to enable overriding what gets searched in the admin search box at runtime. This is also useful if you want to search fields that the interactive module configuration may not let you configure, such as "field.property". 2024-11-01 09:12:36 -04:00
Ryan Cramer
6cd8516a6f Add several methods to SelectableOptionArray class, which represents FieldtypeOptions field values (checkboxes, radios, selects, etc.). Now it is much simpler to get, add, remove options from the selection by referring to the option ID, vlue or title. The added methods include: getByID($id), getByValue($value), getByTitle($title), addByID($id), addByValue($value), addByTitle($title), removeByID($id), removeByValue($value), removeByTitle($title) 2024-10-11 16:20:01 -04:00
Ryan Cramer
9dbd7dd079 Add $page->saveFields([ 'field1, 'field2', 'etc.' ]) method as a front-end to the added $pages->saveFields($page, [ … ]) method 2024-10-11 16:15:27 -04:00
Ryan Cramer
0ef8a4de0b Add new $pages->saveFields([ 'field1', 'field2', 'etc.' ]); method that enables you to save multiple fields on a page. Previously you could only save the entire page, or 1 field at a time. 2024-10-11 16:13:36 -04:00
Ryan Cramer
1c0aa2d248 Attempt fix for processwire/processwire-issues#1974 2024-10-04 15:19:48 -04:00
Ryan Cramer
4f7161fd49 Fix issue processwire/processwire-issues#1976 2024-10-04 14:35:31 -04:00
Ryan Cramer
5abf2077c7 Fix issue processwire/processwire-issues#1977 2024-10-04 14:30:31 -04:00
Ryan Cramer
6d479ba52c Update WireCacheDatabase to improve the efficiency of the general cache maintenance by adding a custom maintenance() method rather than relying on the slower one in WireCache. This also corrects the issue of past caches with an 0000-00-00 expiration date that would never expire, ensuring they don't stick around any longer. 2024-09-19 11:45:06 -04:00
BernhardBaumrock
fef2a76f39 Add PR #302 feat: add support for ajaxParams in ProcessPageList() 2024-09-13 14:03:25 -04:00
Ryan Cramer
965f956bc3 Add support for matching data attributes in show-if conditions. Also updated FieldtypeOptions to add data-if-value attribute to select <option> tags as a way use it. This attribute contains the option value (when separate option values and labels are used). This enables you to match by value rather than by option ID. Example show-if selector: colors.data-if-value=blue. Previously you could only match by option ID, i.e. colors=123. Maybe we'll add something similar for page refernce fields so that you can match by page name or path, rather than by ID. 2024-09-13 13:29:09 -04:00
Ryan Cramer
754b1fffb7 Update ProcessTemplate not to detect hidden files when checking for new templates 2024-09-06 15:09:36 -04:00
Ryan Cramer
fae4fac013 Minor fix in InputfieldTinyMCESettings class 2024-09-06 15:07:55 -04:00
Ryan Cramer
c4257ee646 Typo fix in the phpdoc of InputfieldDatetime 2024-09-06 15:06:37 -04:00
Ryan Cramer
d9399bc673 Typo fix in phpdoc of WireMarkupRegions 2024-09-06 15:04:22 -04:00
Ryan Cramer
6ff9109583 Typo fix in phpdoc of Module.php 2024-09-06 15:03:44 -04:00
Ryan Cramer
ffddd85566 Add /vendor/ to default exclusion list of FileCompiler 2024-09-06 15:03:17 -04:00
Ryan Cramer
67da683ff6 Fix issue processwire/processwire-issues#1965 2024-09-06 14:53:49 -04:00
Ryan Cramer
fcc0e72868 Fix issue in PageFrontEdit when two different pages being saved at the same time 2024-09-05 17:35:00 -04:00
Ryan Cramer
842eca45b9 Fix issue in InputfieldSelector where it could display primary field label rather than template-context field label (when present) in situation where only one template is allowed and 'showFieldLabels' option is enabled. 2024-09-01 11:15:03 -04:00
Ryan Cramer
80f425f9da Minor adjustment for header actions in inputfields.js 2024-08-23 14:24:06 -04:00
Ryan Cramer
e51ece23fe Update the column width slider in the fields list in ProcessTemplate to work more easily. I'm talking about the part where you click and hold the percent and then drag your mouse left/right or up/down to adjust the column width value. It's now using 5% increments (plus 33%, 34% and 66%), and [when or if needed] you can now double click the percent to open the dedicated range slider in a modal, for more specific column width numbers. 2024-08-23 14:20:38 -04:00
Ryan Cramer
95bd1d426c Update asmSelect to trigger 'asmItemUpdated' JS event when the text in one of selected items is modified 2024-08-23 14:06:18 -04:00
Ryan Cramer
9e8ffac63f Some refactoring in InputfieldWrapper to improve markup attribute population. This should also fix processwire/processwire-issues#1958 2024-08-16 11:22:23 -04:00
Ryan Cramer
f77dd242dc Bump version to 3.0.241 2024-08-09 14:59:04 -04:00
Ryan Cramer
ce01e699e3 Fix issue processwire/processwire-issues#1925 using something like PR #288 via @daun
Co-authored-by: daun <post@philippdaun.net>
2024-08-09 14:29:13 -04:00
tobaco
1fdc61dc41 Add PR #291 - remove php version check for strftime call in WireDateTime 2024-08-09 14:20:15 -04:00
Ryan Cramer
6e93844c19 Update FieldtypeFloat to support configurable column type of either 'float' or 'double'. Previously it only supported 'float' unless you manually changed the DB to use double. 2024-08-09 14:07:57 -04:00
Ryan Cramer
2a84f12018 Add new PageFrontEdit::getAjaxPostUrl() hookable method 2024-07-30 11:39:18 -04:00
Ryan Cramer
137b2aa50b Accidentally added some unused code in the last commit so this one removes it. 2024-07-30 11:21:51 -04:00
Ryan Cramer
19fb83201d Update PagesEditor::delete() method to track already-deleted pages to prevent duplicate calls from nested repeaters and such. 2024-07-30 11:20:16 -04:00
Ryan Cramer
bda807a574 Add a getPageInfo method to PagesRequest class per request 2024-07-19 12:45:30 -04:00
Ryan Cramer
5b0e37e3ae Add support for Inputfield header dropdown menu actions. These expand upon the existing Inputfield header actions added in 3.0.240 and enable you to have dropdown menus contain more actions within them. 2024-07-19 12:43:41 -04:00
Ryan Cramer
962d26a749 Small improvements to the pw-dropdowns code in /wire/templates-admin/main.js. Fixes one jQuery 3.x error, adds code to prevent double initialization, and adds code to trigger a pw-show-dropdown JS event when a dropdown is shown. 2024-07-19 12:14:30 -04:00
Ryan Cramer
e508cfa2a7 Minor optimization to Page::setAndSave 2024-07-11 12:15:58 -04:00
Ryan Cramer
4ee947d237 Minor optimization to WireCache 2024-07-11 12:15:45 -04:00
Ryan Cramer
acc7ca2d91 Minor fix to PagesEditor::saveStatus() method 2024-07-05 15:49:14 -04:00
Ryan Cramer
e08fa2e957 Add support for an experimental $config->userOutputFormatting setting and update ProcessProfile to support. 2024-07-05 15:48:25 -04:00
Ryan Cramer
18084dd8ef Forgot to add this file in the last commit 2024-07-05 12:11:48 -04:00
Ryan Cramer
899ffd186a Fix issue processwire/processwire-issues#1944 2024-07-05 12:00:26 -04:00
Ryan Cramer
36227dc778 Fix issue processwire/processwire-issues#1948 2024-07-05 11:35:51 -04:00
Ryan Cramer
2690115966 Several updates for processwire/processwire-issues#1467 2024-07-05 09:50:01 -04:00
Ryan Cramer
98968d796f Fix issue processwire/processwire-issues#1930 2024-07-01 12:24:27 -04:00
Ryan Cramer
dff3e8aaeb Refactor the Templates::getParentPage() method. This should hopefully fix processwire/processwire-issues#1929 ... also removed the Template::noShortcut check from that method since it didn't really belong there, and moved it to ProcessPageList and ProcessPageLister. Some phpdoc updates as well. 2024-07-01 10:59:20 -04:00
Ryan Cramer
d5faf861dc Minor phpdoc updates 2024-06-28 17:15:17 -04:00
Ryan Cramer
0500293f96 Adjustment to Lister to make the spinner more selective about when it spins 2024-06-23 11:19:47 -04:00
Ryan Cramer
7a43790412 Add a checkSystemTimes() method to SystemUpdaterChecks to compare the PHP time to the database time once per superuser session and warn when they differ, with instructions on how to fix it (Thanks Bernhard for the suggestion). 2024-06-20 15:32:09 -04:00
Ryan Cramer
b29e6a45c0 Correction to previous commit 2024-06-20 13:36:46 -04:00
Ryan Cramer
d48588f508 Fix issue processwire/processwire-issues#1927 2024-06-20 13:24:25 -04:00
Ryan Cramer
1222a1598b Attempt fix for processwire/processwire-issues#1926 2024-06-20 12:46:43 -04:00
Ryan Cramer
5609fde13a Fix issue processwire/processwire-issues#1941 2024-06-20 11:56:43 -04:00
Ryan Cramer
92afe679b9 Fix issue processwire/processwire-issues#1936 2024-06-20 10:58:28 -04:00
Ryan Cramer
061170204b Update Lister/ListerPro for improved support of subfield labels 2024-06-20 09:47:45 -04:00
Ryan Cramer
6d225f3c99 Update InputfieldTextTags to support page selection for pages having digit-only titles (such as "2024"). Plus add support for single-page selection mode (previously it only supported multi-page selection mode). 2024-06-20 09:41:40 -04:00
Ryan Cramer
38a5320f61 Bump version to 3.0.240 2024-06-14 15:53:39 -04:00
Ryan Cramer
abe1216c89 Minor fixes 2024-06-14 15:52:28 -04:00
Ryan Cramer
cf0832c330 Add support for custom live search results in the admin search 2024-06-14 15:51:28 -04:00
Ryan Cramer
34c10a5417 Update InputfieldTinyMCE.js to support "saved" and "saveReady" JS events to ensure that TinyMCE content gets populated to original input/textarea elements. Seems to be necessary for LRP but likely to come in handy elsewhere too. 2024-06-14 10:56:34 -04:00
Ryan Cramer
cc79223bc8 Fix issue processwire/processwire-issues#1931 2024-06-07 12:57:33 -04:00
Ryan Cramer
13221c3bd5 Fix issue processwire/processwire-issues#1934 2024-06-07 12:33:43 -04:00
Ryan Cramer
e78ada8854 Update for subfield labels in column headings (primarily for ListerPro) 2024-06-02 12:17:02 -04:00
Ryan Cramer
48f85faced Optimizations to $modules loader 2024-05-31 14:35:46 -04:00
Ryan Cramer
f6a1ea781b Improve support where fields in repeaters (or other embedded types) can have dependencies reference fields outside the repeater by using the "forpage.field_name=..." syntax in teh depedency. For instance, if you want a field in a repeater to only appear if editing a page using template ID 123 then you could use showIf dependency "forpage.template=123". 2024-05-31 14:33:16 -04:00
Ryan Cramer
7988319c72 Adjustment to Paths class 2024-05-31 14:32:28 -04:00
Ryan Cramer
7c85b089dd Update Inputfield class so that dependencies can be supported on many ajax-loaded Inputfields before they are… ajax-loaded. 2024-05-31 14:30:33 -04:00
Ryan Cramer
34bca47a07 Optimizations to WireArray and some descending classes 2024-05-31 14:27:23 -04:00
Ryan Cramer
b9d8a741ee Minor optimizations to ProcessPageEdit 2024-05-31 14:24:08 -04:00
Ryan Cramer
d50cc127cc Minor issue fix in ProcessPageLister.js 2024-05-24 14:52:31 -04:00
Ryan Cramer
904c227cce Optimizations and improvements to $config->demo mode. 2024-05-24 14:51:42 -04:00
Ryan Cramer
00ae62059b Various minor updates in ProcessProfile 2024-05-24 14:49:48 -04:00
Ryan Cramer
9803df9401 Update $database API to have new reset() and close() methods. The reset() method closes and resets the DB connection, while the close() method just closes it. Also updated the execute($query) method to use the reset() method to retry a failed query due to loss of connection. 2024-05-24 14:47:53 -04:00
Ryan Cramer
3c5205721b Add support for PHP-defined header actions for Inputfields as requested by @Toutouwai. These work the same as those defined in JS via Inputfields.addHeaderAction() except the method can now also be called from Inputfield objects in PHP. Also added support for 'link' type actions that open a link in either the current or a modal window. 2024-05-24 14:42:23 -04:00
Ryan Cramer
049efa7c3b Bump version to 3.0.239 2024-05-17 13:41:53 -04:00
Ryan Cramer
212d2b361b Fix issue processwire/processwire-issues#1920 2024-05-17 12:07:39 -04:00
Ryan Cramer
7c89b2b647 Fix issue processwire/processwire-issues#1921 2024-05-17 12:03:16 -04:00
Ryan Cramer
9eb58ead01 Minor phpdoc update in Config.php 2024-05-17 11:11:43 -04:00
Ryan Cramer
faf27c8fa1 Update TemplateFile halt() method to optionally accept string argument to output before halt of template file rendering. For instance: return $this->halt('<h1>See ya</h2>'); from a template file. 2024-05-17 11:09:57 -04:00
Ryan Cramer
764153732e Add new InputfieldWrapper getByField() and getByProperty() methods 2024-05-17 11:09:03 -04:00
Ryan Cramer
172ad1c812 Make the Page::getInputfields() method hookable 2024-05-17 11:08:33 -04:00
Ryan Cramer
eaed402cfb Fix issue processwire/processwire-issues#1918 2024-05-10 12:21:31 -04:00
Ryan Cramer
397bb0b382 Lots of updates to the Inputfield Javascript API (inputfields.js) with several new methods including icon() which can get or set the Inputfield icon, header() which returns the InputfieldHeader element, content() which returns the InputfieldContent element, and addHeaderAction() which lets you add custom icon actions to any Inputfield. Also updated the existing label() method to allow for setting the Inputfield label/header text (previously it could only get). The addHeaderAction() method is the most significant addition, so I'll write more about that in the weekly update. 2024-05-10 11:58:56 -04:00
Ryan Cramer
d77b23adbb Add a new $page->cloneable() method that returns true if the user is allowed to clone the page. Or use $page->cloneable(true) if the user is allowed to clone the page and its children together. This moves the logic was was previously in the ProcessPageClone module into a method that can be more widely used where needed. Also updated the ProcessPageClone module to use it. 2024-05-10 11:52:41 -04:00
Ryan Cramer
4e2ef8f8fd Minor fix in inputfields.js where it wasn't always triggering the 'opened' event when it should 2024-05-03 13:57:13 -04:00
Ryan Cramer
bbaa5570fb Update InputfieldFile and InputfieldImage to support file_context so that the same file/image field can appear more than once in the same editor. This was already supported with for repeaters, but now can be supported by other cases (and is used by PageEditChildren module). This also includes some minor refactoring in InputfieldFile. 2024-05-03 13:54:22 -04:00
Ryan Cramer
dcd820064b Update InputfieldRepeater.js to support more context options beyond nested repeaters and ListerPro. 2024-05-03 13:50:28 -04:00
Ryan Cramer
bae44f93ce Update Page::editUrl() method to support a 'vars' (array) option that contains additional query string variables that it should bundle in to returned URL 2024-05-03 13:48:32 -04:00
Ryan Cramer
c38c204824 Update Fieldgroup::getPageInputfields() to support user-specified $container element (InputfieldWrapper) rather than creating one (as used by PageEditChildren module) 2024-05-03 13:47:50 -04:00
Ryan Cramer
38eadb46d8 Bump version to 3.0.238 2024-04-19 14:44:42 -04:00
Ryan Cramer
4e2d798d49 Add support for custom jQuery UI datepicker settings in InputfieldDatetime per processwire/processwire-requests#523 and also makes several new options interactively configurable in the field settings (Input tab > Datepicker settings fieldset). 2024-04-19 14:37:18 -04:00
Ryan Cramer
a37f237900 Update installer to exclude some $config settings when already supplied by site profile's config.php, so that it's not duplicating any settings when writing /site/config.php 2024-04-19 12:02:01 -04:00
Ryan Cramer
29ecddadeb Update the ProcessWire.alert() javascript function to support an expiration time after which the alert will automatically close. 2024-04-19 11:59:43 -04:00
Ryan Cramer
57b23ef9fe Add $inputfield->setLanguageValue($language, $value) and $inputfield->getLanguageValue($language) to Inputfield class when LanguageSupport is installed. This provides a nicer API for getting/setting multi-language values with Inputfields (where supported). Previously you could only get/set by dealing with "value1234" type settings where 1234 is language ID. 2024-04-19 11:57:39 -04:00
Ryan Cramer
3c0e9f3c43 Add a $datetime->strtodate() function which works like strtotime($str, $format) but returns a formatted date string rather than a timestamp. Also update the $datetime->strtotime() function to accept an inputFormat option which lets you specify the format the given date string is in, for cases when it may not be recognized by PHP based purely on format; and an outputFormat option which lets you specify the format it should return in rather than a timestamp (essentially delegating to the new strtodate method). 2024-04-19 11:53:44 -04:00
Ryan Cramer
def74f7b6d Minor refactor of WireArray::__toString() method 2024-04-19 11:48:07 -04:00
Ryan Cramer
7a85039896 phpdoc improvement in Template.php 2024-04-19 11:47:31 -04:00
Ryan Cramer
432e369990 Minor adjustments in ProcessPageTrash 2024-04-19 11:46:34 -04:00
Ryan Cramer
9a6963a644 Add feature request processwire/processwire-requests#186 which adds the configurable option to always use the full clone form. Also updated it to show a count of how many pages would be cloned when cloning children, and added dropdown options to the submit button so that you can optionally edit a page after cloning. Added icons to all inputs as well. 2024-04-18 11:33:24 -04:00
Ryan Cramer
76388b48e6 Fix ProcessPageClone issue processwire/processwire-issues#1909 plus add option to choose whether children/granchidren/etc are unpublished, and make the getSuggestedNameAndTitle() method hookable 2024-04-18 10:33:13 -04:00
Ryan Cramer
d8ae8f9177 Fix issue processwire/processwire-issues#1904 2024-04-18 09:13:01 -04:00
Ryan Cramer
9e6b89cf93 Fix issue in PagePathFinder where LanguageSupportPageNames module in use without using homepage language segments, combined with multi-language PagePathHistory module, could result in default language incorrectly detected from URL. 2024-04-08 09:08:19 -04:00
Ryan Cramer
7438ae90ca Fix issue processwire/processwire-issues#1903 2024-04-05 13:00:26 -04:00
Ryan Cramer
6aa698343b Fix issue processwire/processwire-issues#1902 plus some code for an unrelated feature I hadn't yet committed 2024-04-05 12:01:51 -04:00
Ryan Cramer
9eb9f88090 Update "Parent" section field in page editor to provide contextual options based on template family settings and access control, when available. This means it now gives you a <select> of allowed parents (when known by family settings) rather than a PageListSelect. This is preferable because a PageListSelect doesn't know about which parent pages are allowed until after submitting the page edit form. 2024-04-05 11:14:00 -04:00
Ryan Cramer
9737b4e15d Update Page::moveable method to also consider its template.parentTemplates setting so that it can return false when no potential parents exist using allowed templates. Also should fix processwire/processwire-issues#1901 2024-04-05 09:56:50 -04:00
Ryan Cramer
37416f8bcc Update PageList to add new classes to items: 'PageListNotPublic' when page is not public for viewing to guest user, 'PageListNoFile' when page has no template file. Plus some other minor optimizationsin while there. 2024-04-03 10:24:48 -04:00
Ryan Cramer
e0f67aa55e Bump version to 3.0.237 2024-03-28 13:24:09 -04:00
Ryan Cramer
91c15f666a Fix issue processwire/processwire-issues#1898 2024-03-28 10:57:16 -04:00
Ryan Cramer
ffdd9729e4 Fix issue processwire/processwire-issues#1892 2024-03-28 10:36:30 -04:00
Ryan Cramer
718c93b056 Fix issue processwire/processwire-issues#1894 2024-03-28 10:30:36 -04:00
Ryan Cramer
68d9ec9b42 Fix issue processwire/processwire-issues#1895 2024-03-28 10:23:18 -04:00
Ryan Cramer
fb12fb7750 Fix issue processwire/processwire-issues#1896 2024-03-28 10:03:16 -04:00
Ryan Cramer
6e1d7b166d Fix issue processwire/processwire-issues#1897 2024-03-28 09:35:40 -04:00
Ryan Cramer
21949387b4 Various minor updates 2024-03-28 09:01:54 -04:00
Ryan Cramer
0852242866 Update default AdminTheme.scss to limit align left/right/center classes to img, figure, div, etc. 2024-03-28 08:59:51 -04:00
Ryan Cramer
4f55480fc7 Update PageValues class to call $page->getField() rather than $this->getField() since it may be overridden by descending Page object (RepeaterMatrixPage for example) 2024-03-28 08:48:21 -04:00
Ryan Cramer
3256cb9000 Update installer so that it works if site profile is already installed in /site/ rather than /site-name/ 2024-03-28 08:36:56 -04:00
Ryan Cramer
55a241e2f1 Fix issue processwire/processwire-issues#1893 2024-03-22 14:45:24 -04:00
Ryan Cramer
5b257c6031 Fix issue processwire/processwire-issues#1899 2024-03-22 14:33:45 -04:00
Ryan Cramer
38757b1baa Fix behavior of PageFinder when selector has multiple fields before a != operator and no values after the operator, i.e. a|b|c!= 2024-03-15 15:50:12 -04:00
Ryan Cramer
d7502b669a New methods for LanguageTranslator class (Note for examples: $translator = $user->language->translator()) - $translator->getTranslationInfo($textdomain, $text) returns verbose array of information about the translation, where it came from, etc. $translator->getTranslationOrFalse($textdomain, $text) returns translation text if translated, or false if not (rather than default language value). $translator->findTranslations($text) searches across all translated files to find all translations for given text. $translator->findTranslation($text) searches across all translated files to find the first available translation for given text. 2024-03-15 15:41:09 -04:00
Ryan Cramer
5fe181c315 Update ProcessPageSearchLive to simplify id searches when using single equals sign 2024-03-01 15:57:20 -05:00
Ryan Cramer
4b55979624 Update ProcessModule to improve the readme URL generation 2024-03-01 15:56:31 -05:00
Ryan Cramer
4099035708 Update PagesVersions to force singular mode 2024-03-01 15:56:08 -05:00
Ryan Cramer
9770138eee Update PageFinder so that joinType ('join' or 'leftjoin') can be modified by the fieldtype when/if needed 2024-03-01 15:55:32 -05:00
Ryan Cramer
04041bb54a Fix issue processwire/processwire-issues#1890 2024-03-01 09:37:50 -05:00
Ryan Cramer
128538fcd8 Refactor of FieldtypeToggle module getMatchQuery() method to add more potential match possibilities but also to fix processwire/processwire-issues#1887 2024-02-29 12:01:53 -05:00
Ryan Cramer
76ad3ab984 Add feature request processwire/processwire-requests#519 2024-02-23 14:17:09 -05:00
Ryan Cramer
f893cec515 Add feature request processwire/processwire-requests#520 - ability do disable (hide) image items in a multiple image field 2024-02-23 13:20:37 -05:00
Ryan Cramer
837a8fd32a Add feature request processwire/processwire-requests#522 which adds the ability to delete webp variations independently of jpg/png variations, including for the main/non-resized image. 2024-02-23 10:57:03 -05:00
Ryan Cramer
37ef2c9070 Add pasteFilter tests in TinyMCE readme file for later reference 2024-02-23 09:36:25 -05:00
Ryan Cramer
9c14e27576 Minor pasteFilter update in InputfieldTinyMCE.js 2024-02-23 09:35:55 -05:00
Ryan Cramer
d5116166d0 Phpdoc documentation updates in RepeaterPageArray.php file 2024-02-23 09:34:04 -05:00
Ryan Cramer
47c639617c Fix issue processwire/processwire-issues#1878 2024-02-21 11:08:07 -05:00
Ryan Cramer
1f2d597f52 Fix issue processwire/processwire-issues#1886 2024-02-21 10:09:47 -05:00
Ryan Cramer
c6dc986f9c Fix issue in PagesPathFinder where it could return a 200 rather than 301 in some cases for multi-language URLs that were missing the leading language segment and 'verbose' mode was disabled. 2024-02-21 09:31:59 -05:00
Ryan Cramer
b5d8a91e49 Fix issue processwire/processwire-issues#1882 2024-02-20 11:08:58 -05:00
Ryan Cramer
f3e614640b Fix issue processwire/processwire-issues#1881 2024-02-20 09:31:26 -05:00
Ryan Cramer
3e90cb74fa Fix issue processwire/processwire-issues#1879 2024-02-20 09:03:47 -05:00
Ryan Cramer
71a1e9c9d9 Bump version to 3.0.236 2024-02-16 08:31:41 -05:00
Ryan Cramer
a53b4e5310 Update inputfields.js dependences so that they can work with Page selection fields (PageAutocomplete, PageListSelect, etc.) when used outside of an InputfieldPage, such as in a module config, field config, etc. Related to processwire/processwire-issues#1873 2024-02-15 12:39:16 -05:00
Ryan Cramer
3ab315dca4 Add an update for processwire/processwire-issues#1873 2024-02-15 12:34:09 -05:00
adrianbj
94653012be Add PR #255 - updates inputfields.js to add support for repeater 'forpage' selectors in showIf dependencies 2024-02-15 10:50:19 -05:00
erikmh
f801fef42b Add PR #279 which fixes a ParseDown extra PHP 8.2 deprecation notice 2024-02-15 10:26:00 -05:00
Ryan Cramer
caa8e7e421 Some upgrades to Repeater and RepeaterMatrix: Added processwire/processwire-requests#474 which enables open/close for family groups of repeater items together, so that when using depths, and you open (or close) a repeater item, items that are visually children of it also open (or close). To enable, see the field "Details" tab setting in "Repeater depths/indents" > "Open/close items as a family?". Also added a configuration setting that enables you do disable the automatic scrolling to newly added items, as requested by @hiboudev 2024-02-14 12:13:54 -05:00
Ryan Cramer
a5f6cabbcf Fix for processwire/processwire-issues#1873 2024-02-13 11:26:02 -05:00
Ryan Cramer
db358ee4db Add loading="lazy" option for <img> tags in Textarea fields (TinyMCE, CKEditor) that is inserted at runtime when output formatting enabled. To turn this on, edit your Textarea field and on the Details tab, see the HTML options. This was added for processwire/processwire-requests#455 2024-02-09 14:28:59 -05:00
Ryan Cramer
215010386f Page::render() and PageRender::renderPage() documentation updates per processwire/processwire-requests#459 2024-02-09 13:23:59 -05:00
Ryan Cramer
97db8e8783 Add hookable InputfieldPage::renderPageLabel() method for processwire/processwire-requests#460 2024-02-09 12:09:45 -05:00
Ryan Cramer
9fe7e95840 Add processwire/processwire-requests#466 which fixes template order in InputfieldSelector. While there, I also updated it to correct field and option order in a couple other spots. 2024-02-09 10:40:50 -05:00
Ryan Cramer
d2fccd84af Update to Page API for getting/setting multi-language values. Added $page->setLanguageValues() and $page->getLanguageValues() for setting and getting values from multiple languages at once. Added $page->setLanguageStatus() and $page->getLanguageStatus() for setting/getting the active status of a language or multiple languages at once (replaces odd calls like $page->status1234 = true). Added $page->setLanguageName() and $page->getLanguageName() for setting/getting the name of a page in a particular language or languages (replaces odd calls like $page->name1234 = 'page-name-in-spanish'). See the method descriptions in the Page class for details on usage. There are also examples of usage in the hook method implemenations in LanguageSupport.module and LanguageSupportPageNames.module. This was added for processwire/processwire-requests#475 but ended up taking it a little further. 2024-02-08 13:15:57 -05:00
Ryan Cramer
660ea79496 Add a few new LazyCron methods: getTimeFuncs(), getTimeFuncName($seconds), adn getTimeFuncSeconds($timeFuncName) for processwire/processwire-requests#485 2024-02-07 10:39:24 -05:00
Ryan Cramer
0a926b58fa Additional updates for ProcessModule README viewer for processwire/processwire-requests#498. Now if you have a ModuleName.README.md file, it will display automatically above the module config settings. This update also makes any relative <a href=/url/> or <img src=file.jpg automatically point to the module's /site/modules/ location, so that an <img src="file.jpg"> automatically updates to <img src="/site/modules/ModuleName/file.jpg">. Links to README.md or CHANGES.md are updated to use the built in-viewer. Other links are converted to target=_blank links, so that they don't take over the iframe used by the modal. You can use {MODULE_INFO.NAME}, {MODULE_INFO.TITLE}, and so on for any module info property, to have it convert automatically to the appropriate value (this was requested by Bernhard). Lastly, this update also adds support for text-only files like README.txt, CHANGELOG.txt, LICENSE.txt. 2024-02-06 12:06:38 -05:00
Ryan Cramer
a3f884146f Add README markdown viewer to ProcessModule per request processwire/processwire-requests#498 2024-02-02 15:07:17 -05:00
Ryan Cramer
8c80c524b1 Add OPTIONS, CONNECT and TRACE to alowed HTTP methods in WireHttp class per processwire/processwire-requests#505 2024-02-02 13:55:42 -05:00
Toutouwai
f02393e538 Add PR #278 which adds new imSaveReady() hookable method to ImageSizerEngineIMagick module 2024-02-02 13:02:20 -05:00
Ryan Cramer
9af0aaf2b2 Add support for <hr> elements in InputfieldSelect/InputfieldSelectMultiple per processwire/processwire-requests#508 2024-02-02 12:50:38 -05:00
Ryan Cramer
ddbbbcc4e6 Optimization in MarkupFieldtype to prevent triggering WireClassLoader unnecessarily 2024-02-02 10:53:04 -05:00
Ryan Cramer
ef3ee4645f Improve error messages in PageValues class when given Page does not have a template assigned. When in debug mode, it also becomes a non-fatal error so that you can more easily fix it. 2024-01-26 14:30:07 -05:00
Ryan Cramer
b41c0dd098 Fix issue processwire/processwire-issues#1870 2024-01-26 14:07:35 -05:00
Ryan Cramer
95e10b89b9 InputfieldTinyMCE: Add support for new "remove all style attributes" option to the Markup Toggle settings. Plus refactoring of the pasteFilter JS in attempt to fix processwire/processwire-issues#1866 which should improve pasting from MS Word. 2024-01-26 13:45:08 -05:00
Ryan Cramer
d37b2d40d7 Bump version to 3.0.235 2024-01-19 16:09:33 -05:00
Ryan Cramer
db04f2d2e6 Reverse CKEditor version change as the new one seems to have some issues and it attempts outbound http requests to ckeditor.com (presumably version checks but may be tracking) 2024-01-19 15:01:05 -05:00
Ryan Cramer
091d875f50 Upgrade CKEditor version from 4.19.0 to 4.22.1 (for InputfieldCKEditor module) 2024-01-19 14:55:17 -05:00
Ryan Cramer
bc888f8b52 Update TinyMCE version from 6.4.1 to 6.8.2 (for InputfieldTinyMCE module) 2024-01-19 14:34:12 -05:00
Ryan Cramer
91eff3074d Add DB socket support to PW installer per request processwire/processwire-issues#1850 2024-01-19 13:53:08 -05:00
Ryan Cramer
511f237429 Add note about default timeout value in WireHttp per processwire/processwire-issues#1868 2024-01-19 10:39:53 -05:00
Ryan Cramer
a3aa5c4dd0 Attempt fix for issue processwire/processwire-issues#1867 2024-01-19 10:31:41 -05:00
Ryan Cramer
3856a200ea Update ProcessPageEdit to show what Page class is being used on the Settings tab > Info fieldset. 2024-01-18 11:26:58 -05:00
Ryan Cramer
1647690bc8 phpdoc updates in FieldtypeRepeater 2024-01-18 11:26:03 -05:00
Ryan Cramer
50a7b4c7c4 Additional update for custom page classes for repeater page items. processwire/processwire-requests#239 2024-01-18 11:24:37 -05:00
Ryan Cramer
1216340a46 Update to previous commit 2024-01-14 11:54:25 -05:00
Ryan Cramer
3717a85f3b Fix issue in InputfieldInteger where it could show an unnecessary error message when using min/max settings and no value has yet been set 2024-01-14 10:25:39 -05:00
Ryan Cramer
bad69efd8e Fix typo in phpdoc since tags 2024-01-12 13:25:47 -05:00
Ryan Cramer
275651bb5a Add support for custom page classes for repeaters (RepeaterPage items). See instructions in the new getCustomPageClass() and setCustomPageClass() methods in FieldtypeRepeater.module. This also is in response to processwire/processwire-requests#239 though a little different than what was discussed in that thread 2024-01-12 13:23:05 -05:00
Ryan Cramer
52da051446 Fix issue in ProcessTemplate where importing fields from another template didn't also import their field-template context customizations 2024-01-12 12:12:16 -05:00
Ryan Cramer
9dec9782e1 Fix issue in file/image fieldtypes presets where default textformatters weren't defined properly 2024-01-12 11:53:05 -05:00
Ryan Cramer
32d425aead Update copyright date in installer footer 2024-01-12 11:52:43 -05:00
Ryan Cramer
baf05a8777 Improvements and fixes to ProcessPageSort module. This should also fix processwire/processwire-issues#1848 2024-01-12 11:51:48 -05:00
Ryan Cramer
8a1f706be9 Add new $pages moveReady(), restoreReady(), and renameReady() hooks. Add option for callback hook on $pages->save(). Improvements to PagesTrash class. Update $pages class so restored() hook does not ever need to be called manually, and update ProcessPageEdit to reflect that. 2024-01-12 11:49:51 -05:00
Ryan Cramer
98fe7f94a0 Add support for a $page->sortPrevious property, which is populated with the old value when a page's sort value is changed at runtime. 2024-01-12 11:45:31 -05:00
Ryan Cramer
ef4444dd7f Bump version to 3.0.234 2024-01-05 15:38:03 -05:00
Ryan Cramer
409c0c0a68 Fix issue processwire/processwire-issues#1851 2024-01-04 13:27:05 -05:00
Ryan Cramer
2cc3960c68 Fix issue processwire/processwire-issues#1852 2024-01-04 13:23:01 -05:00
Ryan Cramer
dd146a4be8 Add PR #274 via @hiboudev which fixes issue processwire/processwire-issues#1855 in InputfieldPageAutocomplete.js
Co-authored-by: hiboudev <hiboudev@gmail.com>
2024-01-04 11:55:51 -05:00
Ryan Cramer
50cf963c88 Upgrade Uikit version in AdminThemeUikit to latest (3.17.11 / Nov 2023) 2024-01-04 11:19:16 -05:00
Ryan Cramer
b61da8575a Fix issue processwire/processwire-issues#1856 to properly support pw-panel tabs in AdminThemeUikit 2024-01-04 10:07:25 -05:00
Ryan Cramer
ee217ee3bd Fix issue processwire/processwire-issues#1857 2024-01-03 12:20:42 -05:00
Ryan Cramer
3220b7dc40 Fix issue processwire/processwire-issues#1862 2024-01-03 11:21:43 -05:00
Ryan Cramer
86c0af08d0 Bump version to 3.0.233 2023-12-22 12:50:08 -05:00
Ryan Cramer
a02020cef0 Various upgrades to the PagesVersions module with the biggest being the addition of partial save/restore. This provides the ability to save or restore some fields and not others. Previously you could only save/restore the entire page version at once. This version also adds support for ProFields Table fields so long as they are non-paginated, and adds partial support for PageTable fields. Note that if a FieldtypeTable or FieldtypeCombo field is using any file/image fields, those don't yet support partial versions, but I have new versions of both that do, which will be released in ProFields soon. 2023-12-22 12:09:36 -05:00
Ryan Cramer
c205d475bf Minor unrelated adjustments 2023-12-22 12:08:28 -05:00
Ryan Cramer
dcb1f47ae3 Update PageTable field for partial version support. Specifically, added or deleted PageTable items are not versioned, but existing items in the PageTable are. Full support should come later. 2023-12-22 12:05:45 -05:00
Ryan Cramer
2e62550133 Update Pagefile, Pagefiles, Pageimage, FieldtypeFile and FieldtypeImage classes to support getFiles() methods that return all files connected with the field, whether originals, variations or extras. 2023-12-22 12:02:34 -05:00
Ryan Cramer
dabc56043f Bump version to 3.0.232 2023-12-15 14:43:00 -05:00
Ryan Cramer
7e7a760b88 Add PagesVersions module to core, which provides an API for managing page versions 2023-12-15 14:17:15 -05:00
Ryan Cramer
99a1d0f81d Update InputfieldImage and ProcessPageEditImageSelect to recognize version in URLs 2023-12-15 13:38:40 -05:00
Ryan Cramer
019d5c6014 Add a removeClass() method to MarkupAdminDataTable 2023-12-15 13:37:28 -05:00
Ryan Cramer
b3d84f15e1 Update TinyMCE and CKEditor modules to recognize versions in URLs 2023-12-15 13:37:01 -05:00
Ryan Cramer
8c11a9939c Add version support to FieldtypeFieldsetPage 2023-12-15 13:34:52 -05:00
Ryan Cramer
d82194816f Move version support in FieldtypeRepeater to separate class, plus add support for nested repeater versions 2023-12-15 13:34:19 -05:00
Ryan Cramer
c62deb7946 Minor fix in PagesEditor class 2023-12-15 13:33:11 -05:00
Ryan Cramer
100711c2f4 Updates to MarkupFieldtype for better handling of Repeater and FieldsetPage markup value rendering 2023-12-15 13:32:42 -05:00
Ryan Cramer
7a512a15a6 Minor adjustment to ProcessPageEdit 2023-12-08 13:51:57 -05:00
Ryan Cramer
92ea8eb074 Update FieldtypeRepeater to implement the FieldtypeDoesVersions interface, and related updates to InputfieldRepeater. This commit also moves some of the export/import methods to a separate FieldtypeRepeaterPorter class. 2023-12-08 13:50:35 -05:00
Ryan Cramer
3e323e5f2f Add the FieldtypeDoesVersions interface for Fieldtypes that support handling versions of their own page data 2023-12-08 13:39:48 -05:00
Ryan Cramer
993b5cc162 Bump version to 3.0.231 2023-11-17 14:21:11 -05:00
Ryan Cramer
4aa7104378 Update Punycode class to not require PHP's mb_string 2023-11-15 12:00:14 -05:00
Ryan Cramer
adac6a1e30 Update PageFrontEdit to behave better with InputfieldTextarea fields that do not support HTML. 2023-11-15 11:58:55 -05:00
Ryan Cramer
8343fd2365 Fix issue processwire/processwire-issues#1818 fix for front-end editor (PageFrontEdit) plain text paste
Co-authored-by: BernhardBaumrock <office@baumrock.com>
2023-11-15 11:48:30 -05:00
Ryan Cramer
c86d39f146 Fix issue processwire/processwire-issues#1832 2023-11-15 09:10:18 -05:00
Ryan Cramer
c2d7b47e12 Fix issue processwire/processwire-issues#1833 2023-11-15 08:33:02 -05:00
Ryan Cramer
ae3287ed33 Fix issue processwire/processwire-issues#1834 2023-11-15 08:27:01 -05:00
Ryan Cramer
260e0f228e Fix issue processwire/processwire-issues#1839 which corrects ProcessLogin refresh issue when client and server have different times for UTC/GMT. 2023-11-14 09:51:33 -05:00
Ryan Cramer
c1df78b0a6 Minor adjustment in ProcessPageEdit 2023-11-10 13:07:58 -05:00
Ryan Cramer
4ffde04a5c Improvements to value rendering mode of InputfieldPageListSelect, InputfieldPageListSelectMultiple, as well as InputfieldSelect and InputfieldText, which are inherited by several others. 2023-11-10 13:06:04 -05:00
Ryan Cramer
3cbae7da97 A couple of minor fixes 2023-11-03 12:30:03 -04:00
Ryan Cramer
e172dd011b Minor phpdoc fix and additions 2023-10-27 14:31:05 -04:00
Ryan Cramer
233a66f846 Update the MarkupQA abstract link feature so that it gets URLs directly from Page objects when the Page::path method is hooked. Previously it would use the $pages->getPath() method which is not aware of hooks to the Page::path method, so could return a different result. 2023-10-27 14:02:44 -04:00
Ryan Cramer
3a6e8ffcda Add OR-group support to Selectors::matches() method so it can be used also with non-page matching in-memory selectors 2023-10-27 13:28:16 -04:00
Ryan Cramer
e53552d8c4 Upgrade Page matching in-memory selectors by adding support for OR-groups, sub-selectors, and same (1) item group matches. 2023-10-27 13:26:24 -04:00
Ryan Cramer
78aea1eedf Bump version to 3.0.230 2023-10-20 15:37:52 -04:00
mpsn
8974100c42 Add PR #276 - add support for certain override options in Pageimage::webp() method 2023-10-19 11:14:41 -04:00
mpsn
db2112defd Add PR #277 unquote setTimeout function calls in inputfields.js 2023-10-19 10:35:14 -04:00
Ryan Cramer
463dd01e66 Various minor unrelated updates 2023-10-19 10:26:23 -04:00
Ryan Cramer
88ad063af1 Minor improvements to ProcessLogger module 2023-10-19 10:21:08 -04:00
Ryan Cramer
996a1b6854 Rewrite of wireIconMarkup() function to expand its capabilities and flexibility 2023-10-19 10:19:02 -04:00
Ryan Cramer
ee6f88dec2 Update for processwire/processwire-issues#1812 2023-10-19 10:07:13 -04:00
Ryan Cramer
1f4d32ded9 Fix issue processwire/processwire-issues#1829 2023-10-19 09:39:26 -04:00
romaincazier
8571be1b23 Fix issue processwire/processwire-issues#1825 2023-10-11 10:37:52 -04:00
Ryan Cramer
6d2c8bf795 Fix issue processwire/processwire-issues#1819 2023-10-06 11:24:14 -04:00
Ryan Cramer
173f1b1b29 Fix issue processwire/processwire-issues#1824 2023-10-06 10:52:48 -04:00
Ryan Cramer
3cc76cc886 Fix the 'Add' label in InputfieldTextTags 2023-09-29 16:46:40 -04:00
Ryan Cramer
3ff60a289c Fix issue processwire/processwire-issues#1821 2023-09-29 16:34:11 -04:00
Ryan Cramer
390ad61ce3 Bump version to 3.0.229 2023-09-29 15:37:24 -04:00
Ryan Cramer
4355654d16 Updates for processwire/processwire-issues#1467 2023-09-29 15:09:30 -04:00
Ryan Cramer
d68c782c8d Adjustment to InputfieldImage filename tooltip 2023-09-29 10:28:57 -04:00
Ryan Cramer
96c7ecfb34 Fix issue processwire/processwire-issues#1817 by adding support for translatable 'Add' label in InputfieldTextTags module 2023-09-29 08:59:05 -04:00
Ryan Cramer
3ba7e2f483 Fix issue processwire/processwire-issues#1816 by adding support for cloning fieldgroup/field context settings in Fieldgroups class 2023-09-29 08:27:27 -04:00
Ryan Cramer
ec2777432d Fix PHP8 deprecation notice in InputfieldInteger 2023-09-29 07:59:34 -04:00
Ryan Cramer
a1ebb5d0df Fix issue in InputfieldSelector with Lister bookmark fields containing field.subfield in OR conditions 2023-09-29 07:56:13 -04:00
Ryan Cramer
5609935e4e Bump version to 3.0.228 2023-09-22 15:28:08 -04:00
Ryan Cramer
17e07e7859 Minor code updates in various classes 2023-09-22 15:26:45 -04:00
Toutouwai
a0fabd6811 Add faster width/height detection for SVG images in Pageimage class 2023-09-22 09:54:33 -04:00
Ryan Cramer
21e370b7ee Fix issue processwire/processwire-issues#1813 2023-09-22 09:48:36 -04:00
Toutouwai
17d6def379 Update for display of SVG thumbnails in admin InputfieldImage 2023-09-20 09:40:29 -04:00
Ryan Cramer
c6844af963 Fix issue processwire/processwire-issues#1812 2023-09-20 09:09:56 -04:00
Ryan Cramer
6050a7139c Additional updates for processwire/processwire-issues#1814 2023-09-19 10:06:51 -04:00
Ryan Cramer
cb5579a8c9 Fix issue processwire/processwire-issues#1814 2023-09-18 14:23:17 -04:00
Ryan Cramer
dd8f2a5c63 Fix issue processwire/processwire-issues#1811 2023-09-15 14:27:17 -04:00
Ryan Cramer
b0414278f8 Update PagesEditor to throw descriptive exception when attempting to save a NullPage. This is to fix what were previously ambiguous error messages. 2023-09-15 08:22:09 -04:00
Ryan Cramer
36580883d7 Update InputfieldTinyMCE to trigger change events for formatting and image resize actions. This is so that they can be detected by PageAutosave, ProDrafts, UserActivity, or other modules that might track change events. 2023-09-15 08:19:59 -04:00
929 changed files with 71603 additions and 20206 deletions

View File

@@ -40,7 +40,7 @@ development branch.
ProcessWire is a timeless tool for web professionals that has always been
committed to the long term. It started in 2003, gained the name ProcessWire
in 2006, and has been in active development as an open source project since 2010.
Now more than a decade later (2023), were just getting started, as ProcessWire
Now more than a decade later (2025), were just getting started, as ProcessWire
continues to grow and develop into the next 10 years and beyond.
While ProcessWire has been around for a long time, dont feel bad if you havent
@@ -71,9 +71,7 @@ in the ProcessWire forums, subscribe to our
[weekly newsletter](https://processwire.com/community/newsletter/subscribe/)
for the latest ProcessWire news, check out our
[website showcase](https://processwire.com/sites/)
to see what others are building with ProcessWire, and read our
[blog](https://processwire.com/blog/)
to stay up-to-date with the latest ProcessWire versions.
to see what others are building with ProcessWire.
Weekly ProcessWire news is posted by Teppo Koivula on his site
[ProcessWire Weekly](https://weekly.pw).
@@ -129,15 +127,15 @@ replacing your `/wire/` directory with the one from the newer version.
### Pro module version upgrade notes (if applicable)
- [FormBuilder](https://processwire.com/store/form-builder/)
version 0.5.3 or newer recommended.
version 0.5.5 or newer recommended.
- [ListerPro](https://processwire.com/store/lister-pro/)
version 1.1.5 or newer recommended.
version 1.1.6 or newer recommended.
- [ProFields](https://processwire.com/store/pro-fields/)
the latest versions of all ProFields (10 modules) are recommended.
- [LoginRegisterPro](https://processwire.com/store/login-register-pro/)
version 7 or newer recommended.
version 8 or newer recommended.
- [ProCache](https://processwire.com/store/pro-cache/)
version 4.0.3 or newer recommended. After upgrading, go to your ProCache
version 4.0.5 or newer recommended. After upgrading, go to your ProCache
settings in the admin (Setup > ProCache) and see if it suggests any
modifications to your .htaccess file.
@@ -171,14 +169,12 @@ resolved any issues.
* [ProcessWire Support Forums](https://processwire.com/talk/)
* [ProcessWire Weekly News](https://weekly.pw/)
* [ProcessWire Blog](https://processwire.com/blog/)
* [Sites running ProcessWire](https://processwire.com/sites/)
* [Subscribe to ProcessWire Weekly email](https://processwire.com/community/newsletter/subscribe/)
* [Submit your site to our directory](https://processwire.com/sites/submit/)
* [Follow @processwire on X-Twitter](http://twitter.com/processwire/)
* [Contact ProcessWire developer](https://processwire.com/contact/)
* [Report issue](https://github.com/processwire/processwire-issues/issues)
------
Copyright 2023 by Ryan Cramer / Ryan Cramer Design, LLC
Copyright 2025 by Ryan Cramer / Ryan Cramer Design, LLC

View File

@@ -14,7 +14,7 @@
}
],
"require": {
"php": ">=5.5",
"php": ">=7.1",
"ext-gd": "*"
},
"autoload": {

View File

@@ -390,7 +390,7 @@ DirectoryIndex index.php index.html index.htm
RewriteCond %{REQUEST_URI} (^|/)(site|site-[^/]+)/modules/.*\.(php|inc|tpl|module|info\.json)$ [NC,OR]
# Block access to any software identifying txt, markdown or textile files
RewriteCond %{REQUEST_URI} (^|/)(COPYRIGHT|INSTALL|README|htaccess)\.(txt|md|textile)$ [NC,OR]
RewriteCond %{REQUEST_URI} (^|/)(COPYRIGHT|INSTALL|README|CHANGELOG|LICENSE|htaccess)\.(txt|md|textile)$ [NC,OR]
# Block potential arbitrary backup files within site directories for things like config
RewriteCond %{REQUEST_URI} (^|/)(site|site-[^/]+)/(config[^/]*/?|[^/]+\.php.*)$ [NC,OR]

View File

@@ -11,7 +11,7 @@
* If that file exists, the installer will not run. So if you need to re-run this installer for any
* reason, then you'll want to delete that file. This was implemented just in case someone doesn't delete the installer.
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
* https://processwire.com
*
* @todo 3.0.190: provide option for command-line options to install
@@ -47,7 +47,7 @@ class Installer {
* Minimum required PHP version to install ProcessWire
*
*/
const MIN_REQUIRED_PHP_VERSION = '5.3.8';
const MIN_REQUIRED_PHP_VERSION = '7.1.0';
/**
* Test mode for installer development, non destructive
@@ -91,18 +91,19 @@ class Installer {
public function execute() {
if(self::TEST_MODE) {
error_reporting(E_ALL | E_STRICT);
error_reporting(E_ALL);
ini_set('display_errors', 1);
}
// these two vars used by install-head.inc
$title = "ProcessWire " . PROCESSWIRE_INSTALL . " Installer";
$formAction = "./install.php";
require("./wire/modules/AdminTheme/AdminThemeUikit/install-head.inc");
$step = $this->post('step');
if($step === '5') require('./index.php');
require("./wire/modules/AdminTheme/AdminThemeUikit/install-head.inc");
if($step === null) {
$this->welcome();
} else {
@@ -112,7 +113,7 @@ class Installer {
case 1: $this->compatibilityCheck(); break;
case 2: $this->dbConfig(); break;
case 4: $this->dbSaveConfig(); break;
case 5: require("./index.php");
case 5:
/** @var ProcessWire $wire */
$wire->modules->refresh();
$this->adminAccountSave($wire);
@@ -187,7 +188,7 @@ class Installer {
if($dir->isDot() || !$dir->isDir()) continue;
$name = $dir->getBasename();
$path = rtrim($dir->getPathname(), '/') . '/';
if(strpos($name, 'site-') !== 0) continue;
if(strpos($name, 'site-') !== 0 && $name !== 'site') continue;
$passed = true;
foreach($dirTests as $test) if(!is_dir($path . $test)) $passed = false;
foreach($fileTests as $test) if(!file_exists($path . $test)) $passed = false;
@@ -402,11 +403,15 @@ class Installer {
$this->warn("Consider making directory $d writable, at least during development.");
}
}
if(is_writable("./site/config.php")) {
$this->ok("/site/config.php is writable");
if(file_exists("./site/config.php")) {
if(is_writable("./site/config.php")) {
$this->ok("/site/config.php is writable");
} else {
$this->err("/site/config.php must be writable during installation. Please adjust the server permissions before continuing.");
}
} else {
$this->err("/site/config.php must be writable. Please adjust the server permissions before continuing.");
$this->err("Site profile is missing a /site/config.php file.");
}
if(!is_file("./.htaccess") || !is_readable("./.htaccess")) {
@@ -474,11 +479,13 @@ class Installer {
if(!isset($values['dbPort'])) $values['dbPort'] = ini_get("mysqli.default_port");
if(!isset($values['dbUser'])) $values['dbUser'] = ini_get("mysqli.default_user");
if(!isset($values['dbPass'])) $values['dbPass'] = ini_get("mysqli.default_pw");
if(!isset($values['dbEngine'])) $values['dbEngine'] = 'MyISAM';
if(!isset($values['dbEngine'])) $values['dbEngine'] = 'InnoDB';
if(!isset($values['dbSocket'])) $values['dbSocket'] = ini_get("mysqli.default_socket");
if(!isset($values['dbCon'])) $values['dbCon'] = 'Hostname';
if(!$values['dbHost']) $values['dbHost'] = 'localhost';
if(!$values['dbPort']) $values['dbPort'] = 3306;
if(empty($values['dbCharset'])) $values['dbCharset'] = 'utf8';
if(empty($values['dbCharset'])) $values['dbCharset'] = 'utf8mb4';
if($values['dbCharset'] != 'utf8mb4') $values['dbCharset'] = 'utf8';
if($values['dbEngine'] != 'InnoDB') $values['dbEngine'] = 'MyISAM';
@@ -486,22 +493,45 @@ class Installer {
if(strpos($key, 'chmod') === 0) {
$values[$key] = (int) $value;
} else if($key != 'httpHosts') {
$values[$key] = htmlspecialchars($value, ENT_QUOTES, 'utf-8');
$values[$key] = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
}
$this->input('dbName', 'DB Name', $values['dbName']);
$this->input('dbUser', 'DB User', $values['dbUser']);
$this->input('dbPass', 'DB Pass', $values['dbPass'], array('type' => 'password', 'required' => false));
$this->input('dbHost', 'DB Host', $values['dbHost']);
$this->input('dbPass', 'DB Pass', $values['dbPass'], array('type' => 'password', 'required' => false));
$this->select('dbCon', 'Connection', $values['dbCon'], array('Hostname', 'Socket'));
$this->clear();
$this->input('dbHost', 'DB Host', $values['dbHost']);
$this->input('dbPort', 'DB Port', $values['dbPort']);
$this->select('dbCharset', 'DB Charset', $values['dbCharset'], array('utf8', 'utf8mb4'));
$this->select('dbEngine', 'DB Engine', $values['dbEngine'], array('MyISAM', 'InnoDB'));
$this->input('dbSocket', 'DB Socket', $values['dbSocket'], array('width' => 300));
$this->select('dbCharset', 'DB Charset', $values['dbCharset'], array('utf8mb4', 'utf8'));
$this->select('dbEngine', 'DB Engine', $values['dbEngine'], array('InnoDB', 'MyISAM'));
$this->clear();
// automatic required states for host, port and socket
echo "
<script>
jQuery(document).ready(function($) {
let ho = $('input[name=dbHost]'), po = $('input[name=dbPort]'),
so = $('input[name=dbSocket]'), co = $('select[name=dbCon]');
co.on('change', function() {
if(co.val() === 'Hostname') {
ho.prop('required', true).closest('p').show();
po.prop('required', true).closest('p').show();
so.prop('required', false).closest('p').hide();
} else {
ho.prop('required', false).closest('p').hide();
po.prop('required', false).closest('p').hide();
so.prop('required', true).closest('p').show();
}
}).change();
});
</script>
";
$this->p(
"The DB Charset option “utf8mb4” may not be compatible with all 3rd party modules.<br />" .
"The DB Engine option “InnoDB” requires MySQL 5.6.4 or newer.",
array('class' => 'detail', 'style' => 'margin-top:0')
);
@@ -666,27 +696,37 @@ class Installer {
$values['debugMode'] = $this->post('debugMode', 'int');
// db configuration
$fields = array('dbUser', 'dbName', 'dbPass', 'dbHost', 'dbPort', 'dbEngine', 'dbCharset');
$fields = array('dbUser', 'dbName', 'dbPass', 'dbHost', 'dbPort', 'dbSocket', 'dbEngine', 'dbCharset', 'dbCon');
foreach($fields as $field) {
$value = $this->post($field, 'string');
$value = substr($value, 0, 255);
if(strpos($value, "'") !== false) $value = str_replace("'", "\\" . "'", $value); // allow for single quotes (i.e. dbPass)
if($field != 'dbPass') $value = str_replace(array(';', '..', '=', '<', '>', '&', '"', "\t", "\n", "\r"), '', $value);
$values[$field] = trim($value);
}
$values['dbCharset'] = ($values['dbCharset'] === 'utf8mb4' ? 'utf8mb4' : 'utf8');
$values['dbEngine'] = ($values['dbEngine'] === 'InnoDB' ? 'InnoDB' : 'MyISAM');
$values['dbEngine'] = ($values['dbEngine'] === 'InnoDB' ? 'InnoDB' : 'MyISAM');
if(!$values['dbUser'] || !$values['dbName'] || !$values['dbPort']) {
if(empty($values['dbUser']) || empty($values['dbName'])) {
$this->alertErr("Missing database user and/or name");
$this->alertErr("Missing database configuration fields");
} else if($values['dbCon'] === 'Socket' && empty($values['dbSocket'])) {
$this->alertErr("Missing database socket");
} else if($values['dbCon'] === 'Hostname' && (empty($values['dbHost']) || empty($values['dbPort']))) {
$this->alertErr("Missing database host and/or port");
} else {
error_reporting(0);
$dsn = "mysql:dbname=$values[dbName];host=$values[dbHost];port=$values[dbPort]";
if($values['dbCon'] === 'Socket') {
$dsn = "mysql:unix_socket=$values[dbSocket];dbname=$values[dbName]";
} else {
$dsn = "mysql:dbname=$values[dbName];host=$values[dbHost];port=$values[dbPort]";
}
$driver_options = array(
\PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'",
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
@@ -830,6 +870,7 @@ class Installer {
$file = __FILE__;
$time = time();
$host = empty($values['httpHosts']) ? '' : implode(',', $values['httpHosts']);
$s = is_file("./site/config.php") ? file_get_contents("./site/config.php") : '';
if(function_exists('random_bytes')) {
$authSalt = sha1(random_bytes(random_int(40, 128)));
@@ -843,17 +884,27 @@ class Installer {
"\n/**" .
"\n * Installer: Database Configuration" .
"\n * " .
"\n */" .
"\n */";
if($values['dbCon'] === 'Socket') {
$cfg .= "\n\$config->dbSocket = '$values[dbSocket]';";
}
$cfg .=
"\n\$config->dbHost = '$values[dbHost]';" .
"\n\$config->dbName = '$values[dbName]';" .
"\n\$config->dbUser = '$values[dbUser]';" .
"\n\$config->dbPass = '$values[dbPass]';" .
"\n\$config->dbPort = '$values[dbPort]';";
if(!empty($values['dbCharset']) && strtolower($values['dbCharset']) != 'utf8') $cfg .= "\n\$config->dbCharset = '$values[dbCharset]';";
if(!empty($values['dbEngine']) && $values['dbEngine'] == 'InnoDB') $cfg .= "\n\$config->dbEngine = 'InnoDB';";
if(!empty($values['dbCharset']) && strtolower($values['dbCharset']) != 'utf8') {
$cfg .= "\n\$config->dbCharset = '$values[dbCharset]';";
}
if(!empty($values['dbEngine']) && $values['dbEngine'] == 'InnoDB') {
$cfg .= "\n\$config->dbEngine = 'InnoDB';";
}
$cfg .=
if(strpos($s, '$config->userAuthSalt') === false) $cfg .=
"\n" .
"\n/**" .
"\n * Installer: User Authentication Salt " .
@@ -864,7 +915,9 @@ class Installer {
"\n * Do not change this value, or user passwords will no longer work." .
"\n * " .
"\n */" .
"\n\$config->userAuthSalt = '$authSalt'; " .
"\n\$config->userAuthSalt = '$authSalt'; ";
if(strpos($s, '$config->tableSalt') === false) $cfg .=
"\n" .
"\n/**" .
"\n * Installer: Table Salt (General Purpose) " .
@@ -874,7 +927,9 @@ class Installer {
"\n * this value or it may break internal system comparisons that use it. " .
"\n * " .
"\n */" .
"\n\$config->tableSalt = '$tableSalt'; " .
"\n\$config->tableSalt = '$tableSalt'; ";
$cfg .=
"\n" .
"\n/**" .
"\n * Installer: File Permission Configuration" .
@@ -888,13 +943,17 @@ class Installer {
"\n * " .
"\n */" .
"\n\$config->timezone = '$values[timezone]';" .
"\n" .
"\n";
if(strpos($s, '$config->defaultAdminTheme') === false) $cfg .=
"\n/**" .
"\n * Installer: Admin theme" .
"\n * " .
"\n */" .
"\n\$config->defaultAdminTheme = 'AdminThemeUikit';" .
"\n" .
"\n";
if(strpos($s, '$config->installed ') === false) $cfg .=
"\n/**" .
"\n * Installer: Unix timestamp of date/time installed" .
"\n * " .
@@ -904,6 +963,17 @@ class Installer {
"\n */" .
"\n\$config->installed = " . time() . ";" .
"\n\n";
if(strpos($s, '$config->sessionName') === false) $cfg .=
"\n/**" .
"\n * Installer: Session name " .
"\n * " .
"\n * Default session name as used in session cookie. " .
"\n * Note that changing this will automatically logout any current sessions. " .
"\n * " .
"\n */" .
"\n\$config->sessionName = 'pw" . mt_rand(0, 999) . "';" .
"\n\n";
if(!empty($values['httpHosts'])) {
$cfg .=
@@ -927,7 +997,9 @@ class Installer {
"\n */" .
"\n\$config->debug = " . ($values['debugMode'] ? 'true;' : 'false;') .
"\n\n";
if(strpos($s, '<' . '?php') === false) $cfg = '<' . "?php namespace ProcessWire;\n\n" . $cfg;
if(($fp = fopen("./site/config.php", "a")) && fwrite($fp, $cfg)) {
fclose($fp);
$this->alertOk("Saved configuration to ./site/config.php");
@@ -1072,27 +1144,28 @@ class Installer {
*/
protected function profileImportSQL($database, $file1, $file2, array $options = array()) {
$defaults = array(
'dbEngine' => 'MyISAM',
'dbCharset' => 'utf8',
'dbEngine' => 'InnoDB',
'dbCharset' => 'utf8mb4',
);
$options = array_merge($defaults, $options);
if(self::TEST_MODE) return;
$restoreOptions = array();
$replace = array();
if($options['dbEngine'] != 'MyISAM') {
$replace['ENGINE=MyISAM'] = "ENGINE=$options[dbEngine]";
}
if($options['dbCharset'] != 'utf8') {
$replace['CHARSET=utf8'] = "CHARSET=$options[dbCharset]";
if(strtolower($options['dbCharset']) === 'utf8mb4') {
if(strtolower($options['dbEngine']) === 'innodb') {
$replace['(255)'] = '(191)';
$replace['(250)'] = '(191)';
} else {
$replace['(255)'] = '(250)'; // max ley length in utf8mb4 is 1000 (250 * 4)
}
$replace['ENGINE=InnoDB'] = "ENGINE=$options[dbEngine]";
$replace['ENGINE=MyISAM'] = "ENGINE=$options[dbEngine]";
$replace['CHARSET=utf8mb4;'] = "CHARSET=$options[dbCharset];";
$replace['CHARSET=utf8;'] = "CHARSET=$options[dbCharset];";
$replace['CHARSET=utf8 COLLATE='] = "CHARSET=$options[dbCharset] COLLATE=";
if(strtolower($options['dbCharset']) === 'utf8mb4') {
if(strtolower($options['dbEngine']) === 'innodb') {
$replace['(255)'] = '(191)';
$replace['(250)'] = '(191)';
} else {
$replace['(255)'] = '(250)'; // max ley length in utf8mb4 is 1000 (250 * 4)
}
}
if(count($replace)) $restoreOptions['findReplaceCreateTable'] = $replace;
require("./wire/core/WireDatabaseBackup.php");
$backup = new WireDatabaseBackup();
@@ -1196,6 +1269,7 @@ class Installer {
);
foreach($this->findProfiles() as $name => $profile) {
if($name === 'site') continue;
$title = empty($profile['title']) ? $name : $profile['title'];
$items[$name] = array(
'label' => "Remove unused $title site profile (/$name/)",
@@ -1772,10 +1846,6 @@ class Installer {
if($value === null && empty($sanitizer)) return null;
if(version_compare(PHP_VERSION, "5.4.0", "<") && function_exists('get_magic_quotes_gpc')) {
if(get_magic_quotes_gpc()) $value = stripslashes($value);
}
switch($sanitizer) {
case 'intSigned':
$value = (int) $value;
@@ -2002,7 +2072,6 @@ class Installer {
/****************************************************************************************************/
if(!Installer::TEST_MODE && is_file("./site/assets/installed.php")) die("This installer has already run. Please delete it.");
error_reporting(E_ALL | E_STRICT);
error_reporting(E_ALL);
$installer = new Installer();
$installer->execute();
$installer->execute();

View File

@@ -17,7 +17,7 @@
* This file is licensed under the MIT license
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
* https://processwire.com
*
*/

View File

@@ -12,7 +12,7 @@
* You may also make up your own configuration options by assigning them
* in /site/config.php
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
* https://processwire.com
*
*
@@ -523,6 +523,18 @@ $config->sessionHistory = 0;
*/
$config->userAuthHashType = 'sha1';
/**
* Enable output formatting for current $user API variable at boot?
*
* EXPERIMENTAL: May not be compatible with with all usages, so if setting to `true`
* then be sure to test thoroughly on anything that works with $user API variable.
*
* @var bool
* @since 3.0.241
*
*/
$config->userOutputFormatting = false;
/**
* Names (string) or IDs (int) of roles that are not allowed to login
*
@@ -973,7 +985,7 @@ $config->protectCSRF = true;
* @var int
*
*/
$config->maxUrlSegments = 4;
$config->maxUrlSegments = 20;
/**
* Maximum length for any individual URL segment (default=128)
@@ -992,7 +1004,23 @@ $config->maxUrlSegmentLength = 128;
* @var int
*
*/
$config->maxUrlDepth = 30;
$config->maxUrlDepth = 30;
/**
* Long URL response (URL depth, length or segments overflow)
*
* HTTP code that ProcessWire should respond with when it receives more URL segments,
* more URL depth, or longer URL length than what is allowed. Suggested values:
*
* - `404`: Page not found
* - `301`: Redirect to closest allowed URL (permanent)
* - `302`: Redirect to closest allowed URL (temporary)
*
* @var int
* @since 3.0.243
*
*/
$config->longUrlResponse = 404;
/**
* Pagination URL prefix
@@ -1037,6 +1065,14 @@ $config->pageNameCharset = 'ascii';
*
* Please note this whitelist is only used if pageNameCharset is 'UTF8'.
*
* If your ProcessWire version is 3.0.244+ AND your installation date was before 10 Jan 2025,
* AND you are enabling UTF8 page names now, please add the text `v3` (without the quotes)
* at the beginning or end of your pageNameWhitelist. This will ensure that it uses a
* newer/better UTF-8 page name conversion. The older version is buggy on PHP versions 7.4+,
* but is used for existing installations so as not to unexpectedly change any existing page
* names. When a new ProcessWire installation occurs after 5 Jan 2025 it automatically uses
* the newer/better version and does not require anything further.
*
* @var string
*
*/
@@ -1405,8 +1441,8 @@ $config->moduleServiceKey = 'pw301';
*/
$config->moduleInstall = array(
'directory' => 'debug', // allow install from ProcessWire modules directory?
'upload' => 'debug', // allow install by module file upload?
'download' => 'debug', // allow install by download from URL?
'upload' => false, // allow install by module file upload?
'download' => false, // allow install by download from URL?
);
/**
@@ -1612,6 +1648,8 @@ $config->adminEmail = '';
* #property bool compress Compress compiled CSS?
* #property array customLessFiles Custom .less files to include, relative to PW installation root.
* #property string customCssFile Target custom .css file to compile custom .less file(s) to.
* #property bool noDarkMode If theme supports a dark mode, specify true to disable it as an option.
* #property bool noTogcbx If theme supports toggle style checkboxes, disable them.
*
*/
$config->AdminThemeUikit = array(
@@ -1620,6 +1658,8 @@ $config->AdminThemeUikit = array(
'compress' => true,
'customLessFiles' => array('/site/templates/admin.less'),
'customCssFile' => '/site/assets/admin.css',
'noDarkMode' => false,
'noTogcbx' => false,
);
/**

View File

@@ -193,10 +193,16 @@ abstract class AdminThemeFramework extends AdminTheme {
*
*/
public function getHeadline() {
$headline = $this->wire('processHeadline');
if(!$headline) $headline = $this->wire()->page->get('title|name');
$headline = (string) $this->wire('processHeadline');
if(!strlen($headline)) $headline = $this->wire()->page->get('title|name');
if($headline !== 'en' && $this->wire()->languages) $headline = $this->_($headline);
return $this->sanitizer->entities1($headline);
$headline = $this->sanitizer->entities1($headline);
if(strpos($headline, '&lt;icon-') !== false && !$this->wire()->process instanceof WirePageEditor) {
if(preg_match('/&lt;icon-([-a-z0-9]+)&gt;/', $headline, $matches)) {
$headline = str_replace($matches[0], wireIconMarkup($matches[1]), $headline);
}
}
return $headline;
}
/**
@@ -543,7 +549,19 @@ abstract class AdminThemeFramework extends AdminTheme {
*
* This is hookable so that something else could add stuff to it.
* See the method body for details on format used.
*
*
* Supported properties/attributes as of 3.0.248:
*
* - url (href)
* - title (label text)
* - target (html attr)
* - icon (name of icon)
* - permission (required permission)
* - id (html attr)
* - class (html attr)
* - onclick (html attr)
* - data-* (html attr)
*
* @return array
*
*/
@@ -600,6 +618,10 @@ abstract class AdminThemeFramework extends AdminTheme {
if(strpos($httpHost, ':')) $httpHost = preg_replace('/:\d+/', '', $httpHost); // remove port
$browserTitle .= "$httpHost";
}
if(strpos($browserTitle, '<icon-') !== false) {
$browserTitle = preg_replace('/<icon-[-a-z0-9]+>\s*/', '', $browserTitle);
}
return $this->sanitizer->entities1($browserTitle);
}
@@ -676,7 +698,7 @@ abstract class AdminThemeFramework extends AdminTheme {
foreach($notices as $n => $notice) {
/** @var Notice $notice */
$text = $notice->text;
$text = (string) $notice->text;
$allowMarkup = $notice->flags & Notice::allowMarkup;
$groupByType = $options['groupByType'] && !($notice->flags & Notice::noGroup) && !($notice instanceof NoticeError);
@@ -799,8 +821,7 @@ abstract class AdminThemeFramework extends AdminTheme {
*
*/
public function renderExtraMarkup($for) {
static $extras = array();
if(empty($extras)) $extras = $this->getExtraMarkup();
$extras = $this->getExtraMarkup();
return isset($extras[$for]) ? $extras[$for] : '';
}

View File

@@ -45,7 +45,7 @@
* @property array $imageSizerOptions Options to set image sizing defaults. Please see the /wire/config.php file for all options and defaults. #pw-group-images
* @property array $webpOptions Options for webp images. Please see /wire/config.php for all options. #pw-group-images
*
* @property bool $pagefileSecure When used, files in /site/assets/files/ will be protected with the same access as the page. Routines files through a passthrough script. #pw-group-files
* @property bool $pagefileSecure When used, files in /site/assets/files/ will be protected with the same access as the page. Routes files through a passthrough script. Note if applying to existing site it may not affect existing pages and file/image fields until they are accessed or saved. #pw-group-files
* @property string $pagefileSecurePathPrefix One or more characters prefixed to the pathname of protected file dirs. This should be some prefix that the .htaccess file knows to block requests for. #pw-group-files
* @property string $pagefileUrlPrefix Deprecated property that was a string that prefixes filenames in PW URLs, becoming a shortcut to a pages files URL (do not use, here for backwards compatibility only). #pw-internal
*
@@ -91,6 +91,7 @@
* @property int $maxUrlSegments Maximum number of extra stacked URL segments allowed in a page's URL (including page numbers) #pw-group-URLs
* @property int $maxUrlSegmentLength Maximum length of any individual URL segment (default=128). #pw-group-URLs
* @property int $maxUrlDepth Maximum URL/path slashes (depth) for request URLs. (Min=10, Max=60) #pw-group-URLs
* @property int $longUrlResponse Response code when URL segments, depth or length exceeds max allowed. #pw-group-URLs @since 3.0.243
* @property string $wireInputOrder Order that variables with the $input API var are handled when you access $input->var. #pw-group-HTTP-and-input
* @property bool $wireInputLazy Specify true for $input API var to load input data in a lazy fashion and potentially use less memory. Default is false. #pw-group-HTTP-and-input
* @property int $wireInputArrayDepth Maximum multi-dimensional array depth for input variables accessed from $input or 1 to only allow single dimension arrays. #pw-group-HTTP-and-input @since 3.0.178
@@ -156,6 +157,7 @@
*
* @property string $userAuthSalt Salt generated at install time to be used as a secondary/non-database salt for the password system. #pw-group-session
* @property string $userAuthHashType Default is 'sha1' - used only if Blowfish is not supported by the system. #pw-group-session
* @property bool $userOutputFormatting Enable output formatting for current $user API variable at boot? (default=false) #pw-group-session @since 3.0.241
* @property string $tableSalt Additional hash for other (non-authentication) purposes. #pw-group-system @since 3.0.164
*
* @property bool $internal This is automatically set to FALSE when PW is externally bootstrapped. #pw-group-runtime

View File

@@ -80,7 +80,7 @@ class Database extends \mysqli implements WireDatabase {
if($config) {
if($config->dbCharset) $this->set_charset($config->dbCharset);
else if($config->dbSetNamesUTF8) $this->query("SET NAMES 'utf8'");
else if($config->get('dbSetNamesUTF8')) $this->query("SET NAMES 'utf8'");
}
}
@@ -134,12 +134,12 @@ class Database extends \mysqli implements WireDatabase {
*
* Active in ProcessWire debug mode only
*
* @param ProcessWire $wire ProcessWire instance, if omitted returns queries for all instances
* @param ProcessWire|null $wire ProcessWire instance, if omitted returns queries for all instances
* @return array
* @deprecated
*
*/
static public function getQueryLog(ProcessWire $wire = null) {
static public function getQueryLog(?ProcessWire $wire = null) {
if($wire) {
return $wire->database->queryLog();
} else {

View File

@@ -465,7 +465,7 @@ abstract class DatabaseQuery extends WireData {
if(is_array($value)) {
$curValue = array_merge($curValue, $value);
} else {
$curValue[] = trim($value, ", ");
$curValue[] = trim("$value", ", ");
}
$this->set($method, $curValue);
@@ -745,15 +745,10 @@ abstract class DatabaseQuery extends WireData {
if($exception && $options['throw']) {
if($this->wire()->config->allowExceptions) throw $exception; // throw original
$message = (string) $exception->getMessage();
$code = (int) $exception->getCode();
// note: re-throw below complains about wrong arguments if the above two
// lines are called in the line below, so variables are intermediary
throw new WireDatabaseQueryException($message, $code, $exception);
WireException([ 'class' => 'WireDatabaseQueryException', 'previous' => $exception ]);
}
return $options['returnQuery'] ? $query : $result;
}
}

View File

@@ -245,7 +245,7 @@ class DatabaseQuerySelectFulltext extends Wire {
*
*/
protected function escapeLike($str) {
return str_replace(array('%', '_'), array('\\%', '\\_'), $str);
return str_replace(array('%', '_'), array('\\%', '\\_'), "$str");
}
/**
@@ -258,7 +258,7 @@ class DatabaseQuerySelectFulltext extends Wire {
*
*/
protected function escapeAgainst($str) {
$str = str_replace(array('@', '+', '-', '*', '~', '<', '>', '(', ')', ':', '"', '&', '|', '=', '.'), ' ', $str);
$str = str_replace(array('@', '+', '-', '*', '~', '<', '>', '(', ')', ':', '"', '&', '|', '=', '.'), ' ', "$str");
while(strpos($str, ' ')) $str = str_replace(' ', ' ', $str);
return $str;
}
@@ -270,7 +270,7 @@ class DatabaseQuerySelectFulltext extends Wire {
*/
protected function value($value) {
$maxLength = self::maxQueryValueLength;
$value = trim($value);
$value = trim("$value");
if(strlen($value) < $maxLength && strpos($value, "\n") === false && strpos($value, "\r") === false) return $value;
$value = $this->sanitizer->trunc($value, $maxLength);
return $value;
@@ -328,6 +328,10 @@ class DatabaseQuerySelectFulltext extends Wire {
} else {
$this->matchFieldName($fieldName, $value);
}
if(!count($this->query->where) && (strpos($operator, '~') !== false || $operator === '*+=')) {
$this->query->where('(1>2)'); // force non-match
}
return $this;
}
@@ -720,9 +724,10 @@ class DatabaseQuerySelectFulltext extends Wire {
$wordsAlternates = array();
$phraseWords = $this->words($value); // including non-indexable
$lastPhraseWord = array_pop($phraseWords);
$lastPhraseWord = (string) array_pop($phraseWords);
$scoreField = $this->getScoreFieldName();
$againstValues = array();
$matchAgainst = null;
// BOOLEAN PHRASE: full phrase matches come before expanded matches
if(count($phraseWords)) {
@@ -748,19 +753,20 @@ class DatabaseQuerySelectFulltext extends Wire {
}
}
}
$againstValues[] = ($this->isIndexableWord($lastPhraseWord) ? '+' : '') . $this->escapeAgainst($lastPhraseWord) . '*';
$bindKey = $this->query->bindValueGetKey(implode(' ', $againstValues));
$matchAgainst = "$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE)";
if($this->allowOrder) {
$this->query->select("$matchAgainst + 333.3 AS $scoreField");
$this->query->orderby("$scoreField DESC");
if(strlen($lastPhraseWord)) {
$againstValues[] = ($this->isIndexableWord($lastPhraseWord) ? '+' : '') . $this->escapeAgainst($lastPhraseWord) . '*';
$bindKey = $this->query->bindValueGetKey(implode(' ', $againstValues));
$matchAgainst = "$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE)";
if($this->allowOrder) {
$this->query->select("$matchAgainst + 333.3 AS $scoreField");
$this->query->orderby("$scoreField DESC");
}
}
if(!count($words)) {
// no words to work with for query expansion (not likely, unless stopwords or too-short)
$this->query->where($matchAgainst);
if($matchAgainst) $this->query->where($matchAgainst);
return;
}
@@ -1215,6 +1221,7 @@ class DatabaseQuerySelectFulltext extends Wire {
'stopwords' => true, // allow stopwords
'indexable' => false, // include only indexable words?
'alternates' => false, // include alternate versions of words?
'truncate' => true,
);
$options = count($options) ? array_merge($defaults, $options) : $defaults;
@@ -1269,7 +1276,7 @@ class DatabaseQuerySelectFulltext extends Wire {
if(strpos($likeValue, "'") !== false || strpos($likeValue, "") !== false) {
// match either straight or curly apostrophe
$likeValue = preg_replace('/[\']+/', '(\'|)', $likeValue);
$likeValue = str_replace([ "'", "" ], "('|)", $likeValue);
// if word ends with apostrophe then apostrophe is optional
$likeValue = rtrim(str_replace("('|) ", "('|)? ", "$likeValue "));
}
@@ -1312,6 +1319,7 @@ class DatabaseQuerySelectFulltext extends Wire {
*
*/
protected function strlen($value) {
$value = (string) $value;
if(function_exists('mb_strlen')) {
return mb_strlen($value);
} else {

View File

@@ -376,7 +376,7 @@ class Debug {
$obj = null;
$class = '';
$type = '';
$args = $trace['args'];
$args = isset($trace['args']) ? $trace['args'] : array();
$argStr = '';
$file = $trace['file'];
$basename = basename($file);

View File

@@ -8,36 +8,133 @@
* This file is licensed under the MIT license
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
* https://processwire.com
*
*/
/**
* Throw a new WireException functionally
*
* This can be used to facilitiate re-throwing a non-WireException as a WireException,
* notably \PDOException or other exception classes that might use string for `code` property.
*
* ~~~~
* // throw random WireException
* WireException();
*
* // throw with message
* WireException('Hello world');
*
* // throw WirePermissionException
* WireException([ 'class' => 'WirePermissionException', 'message' => 'No access' ]);
*
* // re-throw previous exception as WireException (and inherit message and code)
* WireException([ 'previous' => $exception ]);
* ~~~~
*
* @param array|string $options One of the following options, or string for just `message`:
* - `class` (string): Class name of WireException to throw (default='WireException').
* - `message` (string): Exception message string (default='' or pulled from previous exception).
* - `code` (int|string): Exception code integer or alphanumeric string (default=0 or pulled from previous exception).
* - `previous` (\Throwable): Previous exception. When present, code and message will be pulled from it if not specified.
* @throws WireException
* @since 3.0.248
*
*
*/
function WireException($options = []) {
$defaults = [
'class' => 'WireException',
'message' => is_string($options) ? $options : '',
'code' => 0,
'previous' => null,
];
$options = is_array($options) ? array_merge($defaults, $options) : $defaults;
if($options['previous'] instanceof \Throwable) {
if(empty($options['message'])) {
$options['message'] = $options['previous']->getMessage();
}
if(empty($options['code'])) {
$options['code'] = $options['previous']->getCode();
}
} else {
$options['previous'] = null;
}
$class = wireClassName($options['class'], true);
$e = new $class($options['message'], 0, $options['previous']);
if($e instanceof WireException && $options['code'] !== 0) {
$e->setCode($options['code']);
}
throw $e;
}
/**
* Generic ProcessWire exception
*
*/
class WireException extends \Exception {
/**
* Exception code when a string
*
* @var string
* @since 3.0.248
*
*/
protected $codeStr = '';
/**
* Replace previously set message
*
* Public since 3.0.248
*
* @param string $message
* @since 3.0.150
*
*/
protected function setMessage($message) {
public function setMessage($message) {
$this->message = $message;
}
/**
* Replace previously set code
*
* @param int $code
* Public since 3.0.248
*
* @param int|string $code
* @since 3.0.150
*
*/
protected function setCode($code) {
$this->code = $code;
public function setCode($code) {
if(is_string($code)) {
$this->setCodeStr($code);
if(ctype_digit($code)) $this->code = (int) $code;
} else {
$this->code = (int) $code;
}
}
/**
* Set alphanumeric code string
*
* @param string $codeStr
* @since 3.0.248
*
*/
public function setCodeStr($codeStr) {
$this->codeStr = (string) $codeStr;
}
/**
* Get alphanumeric/string code if set, blank string if not
*
* @return string
* @since 3.0.248
*
*/
public function getCodeStr() {
return $this->codeStr;
}
}
@@ -126,6 +223,7 @@ class WireDatabaseException extends WireException {}
*
* May have \PDOException populated with call to its getPrevious(); method,
* in which can it also has same getCode() and getMessage() as \PDOException.
* Use getCodeStr() for PDOException string code.
*
* @since 3.0.156
*
@@ -169,5 +267,3 @@ class PageFinderException extends WireException { }
*
*/
class PageFinderSyntaxException extends PageFinderException { }

View File

@@ -879,7 +879,7 @@ class Field extends WireData implements Saveable, Exportable {
* @return bool True if viewable, false if not
*
*/
public function ___viewable(Page $page = null, User $user = null) {
public function ___viewable(?Page $page = null, ?User $user = null) {
return $this->wire()->fields->_hasPermission($this, 'view', $page, $user);
}
@@ -893,12 +893,12 @@ class Field extends WireData implements Saveable, Exportable {
*
* #pw-group-access
*
* @param Page|string|int|null $page Optionally specify a Page for context
* @param User|string|int|null $user Optionally specify a different user (default = current user)
* @param Page|null $page Optionally specify a Page for context
* @param User|null $user Optionally specify a different user (default = current user)
* @return bool
*
*/
public function ___editable(Page $page = null, User $user = null) {
public function ___editable(?Page $page = null, ?User $user = null) {
return $this->wire()->fields->_hasPermission($this, 'edit', $page, $user);
}
@@ -1065,7 +1065,13 @@ class Field extends WireData implements Saveable, Exportable {
foreach(array('showIf', 'requiredIf') as $depType) {
$theIf = $inputfield->getSetting($depType);
if(empty($theIf)) continue;
$inputfield->set($depType, preg_replace('/([_.|a-zA-Z0-9]+)([=!%*<>]+)/', '$1' . $contextStr . '$2', $theIf));
$theIf = preg_replace('/([_|a-zA-Z0-9]+)*([-._|a-zA-Z0-9]*)([=!%*<>]+)/', '$1' . $contextStr . '$2$3', $theIf);
if(stripos($theIf, 'forpage.') !== false) {
// de-contextualize if the field name starts with 'forpage.' as used by
// repeaters (or others) referring to page in editor rather than item page
$theIf = preg_replace('/forpage\.([_.|a-z0-9]+)' . $contextStr . '([=!%*<>]+)/i', '$1$2', $theIf);
}
$inputfield->set($depType, $theIf);
}
}
@@ -1242,7 +1248,9 @@ class Field extends WireData implements Saveable, Exportable {
$table = $this->setTable;
} else {
$name = $this->settings['name'];
if(!strlen($name)) throw new WireException("Field 'name' is required");
$length = strlen($name);
if(!$length) throw new WireException("Field 'name' is required");
if($length > 58) $name = substr($name, 0, 58); // 'field_' + 58 = 64 max
$table = self::tablePrefix . $name;
}
if(self::$lowercaseTables) $table = strtolower($table);
@@ -1259,6 +1267,7 @@ class Field extends WireData implements Saveable, Exportable {
*/
public function setTable($table = null) {
$table = empty($table) ? '' : $this->wire()->sanitizer->fieldName($table);
if(strlen($table) > 64) $table = substr($table, 0, 64);
$this->setTable = $table;
}

View File

@@ -82,7 +82,10 @@ class FieldSelectorInfo extends Wire {
// when input=select, page or checkbox, this contains the selectable options (value => label)
'options' => array(),
// if field has subfields, this contains array of all above, indexed by subfield name (blank if not applicable)
'subfields' => array(),
'subfields' => array(
// same as above, plus… DB column name (if different from 'name')
// 'col' => '',
),
);
$this->schemaToInput = array(

View File

@@ -518,6 +518,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
* - `namespace` (string): Additional namespace for Inputfield context.
* - `flat` (bool): Return all Inputfields in a flattened InputfieldWrapper?
* - `populate` (bool): Populate page values to Inputfields? (default=true) since 3.0.208
* - `container` (InputfieldWrapper): The InputfieldWrapper element to add fields into, or omit for new. since 3.0.239
* @param string|array $fieldName Limit to a particular fieldName(s) or field IDs (optional).
* - If specifying a single field (name or ID) and it refers to a fieldset, then all fields in that fieldset will be included.
* - If specifying an array of field names/IDs the returned InputfieldWrapper will maintain the requested order.
@@ -536,6 +537,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
'namespace' => $namespace,
'flat' => $flat,
'populate' => true, // populate page values?
'container' => null,
);
$options = $contextStr;
$options = array_merge($defaults, $options);
@@ -544,11 +546,16 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
$namespace = $options['namespace'];
$populate = (bool) $options['populate'];
$flat = $options['flat'];
$container = $options['container'];
} else {
$populate = true;
$container = null;
}
if(!$container instanceof InputfieldWrapper) {
$container = $this->wire(new InputfieldWrapper());
}
$container = $this->wire(new InputfieldWrapper());
$containers = array();
$inFieldset = false;
$inHiddenFieldset = false;
@@ -777,5 +784,3 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
}
}

View File

@@ -7,7 +7,7 @@
* #pw-body For full details on all methods available in a Fieldgroup, be sure to also see the `WireArray` class.
* #pw-var $fieldgroups
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @method Fieldgroup clone(Saveable $item, $name = '')
@@ -349,19 +349,46 @@ class Fieldgroups extends WireSaveableItemsLookup {
*
* @param Saveable $item Item to clone
* @param string $name
* @return bool|Saveable $item Returns the new clone on success, or false on failure
* @return Saveable|Fieldgroup
* @return Fieldgroup|false $item Returns the new clone on success, or false on failure
*
*/
public function ___clone(Saveable $item, $name = '') {
return parent::___clone($item, $name);
// @TODO clone the field context data
/*
$id = $item->id;
$item = parent::___clone($item);
if(!$item) return false;
return $item;
*/
if(!$item instanceof Fieldgroup) return false;
$database = $this->wire()->database;
/** @var Fieldgroup|false $fieldgroup */
$fieldgroup = parent::___clone($item, $name);
if(!$fieldgroup) return false;
$sql =
'SELECT fields_id, sort, data FROM fieldgroups_fields ' .
'WHERE fieldgroups_id=:fieldgroups_id ' .
'AND data IS NOT NULL';
$query = $this->wire()->database->prepare($sql);
$query->bindValue(':fieldgroups_id', $item->id, \PDO::PARAM_INT);
$query->execute();
$rows = $query->fetchAll(\PDO::FETCH_ASSOC);
$query->closeCursor();
$sql =
'UPDATE fieldgroups_fields SET data=:data ' .
'WHERE fieldgroups_id=:fieldgroups_id ' .
'AND fields_id=:fields_id AND sort=:sort';
$query = $database->prepare($sql);
foreach($rows as $row) {
$query->bindValue(':data', $row['data']);
$query->bindValue(':fieldgroups_id', (int) $fieldgroup->id, \PDO::PARAM_INT);
$query->bindValue(':fields_id', (int) $row['fields_id'], \PDO::PARAM_INT);
$query->bindValue(':sort', (int) $row['sort'], \PDO::PARAM_INT);
$query->execute();
}
return $fieldgroup;
}
/**
@@ -566,12 +593,12 @@ class Fieldgroups extends WireSaveableItemsLookup {
* #pw-internal
*
* @param Field $field
* @param Template $template
* @param Fieldgroup $fieldgroup
* @param Template|null $template
* @return bool|string Returns error message string if not removeable or boolean false if it is removeable
*
*/
public function isFieldNotRemoveable(Field $field, Fieldgroup $fieldgroup, Template $template = null) {
public function isFieldNotRemoveable(Field $field, Fieldgroup $fieldgroup, ?Template $template = null) {
if(is_null($template)) $template = $this->wire()->templates->get($fieldgroup->name);
@@ -619,4 +646,3 @@ class Fieldgroups extends WireSaveableItemsLookup {
*/
public function ___fieldRemoved(Fieldgroup $fieldgroup, Field $field) { }
}

View File

@@ -5,7 +5,7 @@
*
* WireArray of Fieldgroup instances as used by Fieldgroups class.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
*
@@ -25,6 +25,7 @@ class FieldgroupsArray extends WireArray {
*
*/
public function getItemKey($item) {
/** @var Fieldgroup $item */
return $item->id;
}

View File

@@ -5,7 +5,7 @@
*
* Manages collection of ALL Field instances, not specific to any particular Fieldgroup
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* #pw-summary Manages all custom fields in ProcessWire, independently of any Fieldgroup.
@@ -107,6 +107,13 @@ class Fields extends WireSaveableItems {
*/
protected $flagNames = array();
/**
* Flags to field IDs
*
* @var array
*/
protected $flagsToIds = array();
/**
* Field names that are native/permanent to this instance of ProcessWire (configurable at runtime)
*
@@ -184,6 +191,50 @@ class Fields extends WireSaveableItems {
return $this->wire(new Field());
}
/**
* Called after rows loaded from DB but before populated to this instance
*
* @param array $rows
*
*/
protected function loadRowsReady(array &$rows) {
for($flag = 1; $flag <= 256; $flag *= 2) {
$this->flagsToIds[$flag] = array();
}
foreach($rows as $row) {
$flags = (int) $row['flags'];
if(empty($flags)) continue;
foreach($this->flagsToIds as $flag => $ids) {
if($flags & $flag) $this->flagsToIds[$flag][] = (int) $row['id'];
}
}
}
/**
* Given field ID return native property
*
* This avoids loading the field if the property can be obtained natively.
*
* #pw-internal
*
* @param int $id
* @param string $property
* @return array|bool|mixed|string|null
* @since 3.0.243
*
*/
public function fieldIdToProperty($id, $property) {
$id = (int) $id;
if(isset($this->lazyIdIndex[$id])) {
$n = $this->lazyIdIndex[$id];
if(isset($this->lazyItems[$n][$property])) {
return $this->lazyItems[$n][$property];
}
}
$field = $this->get($id);
return $field ? $field->get($property) : null;
}
/**
* Make an item and populate with given data
*
@@ -250,7 +301,7 @@ class Fields extends WireSaveableItems {
* @since 3.0.194
*
*/
protected function initItem(array &$row, WireArray $items = null) {
protected function initItem(array &$row, ?WireArray $items = null) {
/** @var Field $item */
$item = parent::initItem($row, $items);
$fieldtype = $item ? $item->type : null;
@@ -317,7 +368,7 @@ class Fields extends WireSaveableItems {
* $fields->save($field);
* ~~~~~
*
* @param Field|Saveable $item The field to save
* @param Field $item The field to save
* @return bool True on success, false on failure
* @throws WireException
*
@@ -337,12 +388,14 @@ class Fields extends WireSaveableItems {
// even if only the case has changed.
$schema = $item->type->getDatabaseSchema($item);
if(!empty($schema)) {
foreach(array($table, "tmp_$table") as $t) {
list(,$tmpTable) = explode(Field::tablePrefix, $table, 2);
$tmpTable = "tempf_$tmpTable";
foreach(array($table, $tmpTable) as $t) {
if(!$database->tableExists($t)) continue;
throw new WireException("Cannot rename to '$item->name' because table `$table` already exists");
}
$database->exec("RENAME TABLE `$prevTable` TO `tmp_$table`"); // QA
$database->exec("RENAME TABLE `tmp_$table` TO `$table`"); // QA
$database->exec("RENAME TABLE `$prevTable` TO `$tmpTable`"); // QA
$database->exec("RENAME TABLE `$tmpTable` TO `$table`"); // QA
}
$item->prevTable = '';
}
@@ -432,7 +485,7 @@ class Fields extends WireSaveableItems {
*
* This method will throw a WireException if you attempt to delete a field that is currently in use (i.e. assigned to one or more fieldgroups).
*
* @param Field|Saveable $item Field to delete
* @param Field $item Field to delete
* @return bool True on success, false on failure
* @throws WireException
*
@@ -473,7 +526,7 @@ class Fields extends WireSaveableItems {
/**
* Create and return a cloned copy of the given Field
*
* @param Field|Saveable $item Field to clone
* @param Field $item Field to clone
* @param string $name Optionally specify name for new cloned item
* @return Field $item Returns the new clone on success, or false on failure
*
@@ -658,6 +711,7 @@ class Fields extends WireSaveableItems {
$field2->flags = 0; // intentional overwrite after above line
}
$field2->name = $field2->name . "_PWTMP";
$field2->prevFieldtype = $field1->type;
$field2->type->createField($field2);
$field1->type = $field1->prevFieldtype;
@@ -1092,6 +1146,30 @@ class Fields extends WireSaveableItems {
return $items;
}
/**
* Find fields by flag
*
* #pw-internal
*
* @param int $flag
* @param bool $getFieldNames
* @return array|Field[]
* @since 3.0.243
*
*/
public function findByFlag($flag, $getFieldNames = false) {
if(!isset($this->flagsToIds[$flag])) return array();
$items = [];
foreach($this->flagsToIds[$flag] as $id) {
if($getFieldNames) {
$items[] = $this->fieldIdToProperty($id, 'name');
} else {
$items[] = $this->get($id);
}
}
return $items;
}
/**
* Find fields by type
*
@@ -1238,7 +1316,7 @@ class Fields extends WireSaveableItems {
*
* #pw-internal
*
* @param Field|int|string Field to check
* @param Field Field to check
* @param string $permission Specify either 'view' or 'edit'
* @param Page|null $page Optionally specify a page for context
* @param User|null $user Optionally specify a user for context (default=current user)
@@ -1246,7 +1324,7 @@ class Fields extends WireSaveableItems {
* @throws WireException if given invalid arguments
*
*/
public function _hasPermission(Field $field, $permission, Page $page = null, User $user = null) {
public function _hasPermission(Field $field, $permission, ?Page $page = null, ?User $user = null) {
if($permission != 'edit' && $permission != 'view') {
throw new WireException('Specify either "edit" or "view"');
}
@@ -1284,7 +1362,7 @@ class Fields extends WireSaveableItems {
*
* #pw-hooker
*
* @param Field|Saveable $item
* @param Field $item
* @param Fieldtype $fromType
* @param Fieldtype $toType
*
@@ -1296,7 +1374,7 @@ class Fields extends WireSaveableItems {
*
* #pw-hooker
*
* @param Field|Saveable $item
* @param Field $item
* @param Fieldtype $fromType
* @param Fieldtype $toType
*

View File

@@ -766,7 +766,7 @@ abstract class Fieldtype extends WireData implements Module {
$database = $this->wire()->database;
if(!$database->isOperator($operator)) {
throw new WireException("Operator '$operator' is not implemented in $this->className");
throw new PageFinderSyntaxException("Operator '$operator' is not implemented in $this->className");
}
$table = $database->escapeTable($table);
@@ -1378,12 +1378,13 @@ abstract class Fieldtype extends WireData implements Module {
$result = $query->execute();
} catch(\PDOException $e) {
if($e->getCode() == 23000) {
$code = (int) $e->getCode();
if($code === 23000) {
$message = sprintf(
$this->_('Value not allowed for field “%s” because it is already in use'),
$field->name
);
throw new WireDatabaseException($message, $e->getCode(), $e);
throw new WireDatabaseException($message, $code, $e);
} else {
throw $e;
}
@@ -1676,4 +1677,3 @@ abstract class Fieldtype extends WireData implements Module {
}

View File

@@ -251,7 +251,7 @@ abstract class FieldtypeMulti extends Fieldtype {
} catch(\Exception $e) {
if($useTransaction) $database->rollBack();
if($config->allowExceptions) throw $e; // throw original
throw new WireDatabaseQueryException($e->getMessage(), $e->getCode(), $e);
WireException([ 'class' => 'WireDatabaseQueryException', 'previous' => $e ]);
}
if(!count($values)) {
@@ -343,7 +343,7 @@ abstract class FieldtypeMulti extends Fieldtype {
/** @var \PDOException $exception */
if($useTransaction) $database->rollBack();
if($config->allowExceptions) throw $exception; // throw original
throw new WireDatabaseQueryException($exception->getMessage(), $exception->getCode(), $exception);
WireException([ 'class' => 'WireDatabaseQueryException', 'previous' => $exception ]);
} else {
if($useTransaction) $database->commit();
}
@@ -503,7 +503,7 @@ abstract class FieldtypeMulti extends Fieldtype {
} else if($col === 'limit') {
$value = (int) $value;
if($value > 0) $limit = $value;
if($value > -1) $limit = $value;
} else if($col === 'start') {
$value = (int) $value;
@@ -866,18 +866,24 @@ abstract class FieldtypeMulti extends Fieldtype {
*
*/
public function getLoadQueryAutojoin(Field $field, DatabaseQuerySelect $query) {
$database = $this->wire()->database;
if($this->get('useOrderByCols')) {
// autojoin is not used if sorting or pagination is active
$orderByCols = $field->get('orderByCols');
if(count($orderByCols) > 0) return null;
}
$table = $this->database->escapeTable($field->table);
$schema = $this->trimDatabaseSchema($this->getDatabaseSchema($field));
$fieldName = $this->database->escapeCol($field->name);
$table = $database->escapeTable($field->table);
$schemaAll = $this->getDatabaseSchema($field);
$schema = $this->trimDatabaseSchema($schemaAll);
$fieldName = $database->escapeCol($field->name);
$separator = self::multiValueSeparator;
if($field->distinctAutojoin) $table = "DISTINCT $table";
$orderBy = '';
if($field->distinctAutojoin) {
if(isset($schemaAll['sort'])) $orderBy = "ORDER BY $table.sort";
$table = "DISTINCT $table";
}
foreach($schema as $key => $unused) {
$query->select("GROUP_CONCAT($table.$key SEPARATOR '$separator') AS `{$fieldName}__$key`"); // QA
$query->select("GROUP_CONCAT($table.$key $orderBy SEPARATOR '$separator') AS `{$fieldName}__$key`"); // QA
}
return $query;
}
@@ -1039,5 +1045,3 @@ abstract class FieldtypeMulti extends Fieldtype {
}
}

View File

@@ -6,7 +6,7 @@
* #pw-summary Maintains a collection of Fieldtype modules.
* #pw-var $fieldtypes
*
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* @property FieldtypeCheckbox $FieldtypeCheckbox
@@ -88,13 +88,24 @@ class Fieldtypes extends WireArray {
*/
protected $isAPI = false;
/**
* Construct
*
*/
public function __construct() {
parent::__construct();
$this->usesNumericKeys = false;
$this->indexedByName = true;
$this->nameProperty = 'className';
}
/**
* Construct the $fieldtypes API var (load all Fieldtype modules into it)
*
*/
public function init() {
$this->isAPI = true;
foreach($this->wire()->modules->findByPrefix('Fieldtype', 3) as $name => $module) {
foreach($this->wire()->modules->findByPrefix('Fieldtype', 3) as /* $name => */ $module) {
$this->add($module);
}
}
@@ -153,16 +164,6 @@ class Fieldtypes extends WireArray {
return $item->className();
}
/**
* Does this WireArray use numeric keys only?
*
* @return bool
*
*/
protected function usesNumericKeys() {
return false;
}
/**
* Per the WireArray interface, return a blank copy
*
@@ -229,5 +230,3 @@ class Fieldtypes extends WireArray {
public function getNext($item, $strict = true) { $this->preload(); return parent::getNext($item, $strict); }
public function getPrev($item, $strict = true) { $this->preload(); return parent::getPrev($item, $strict); }
}

View File

@@ -187,6 +187,7 @@ class FileCompiler extends Wire {
}
$this->addExclusion($config->paths->wire);
$this->addExclusion($config->paths->root . 'vendor/');
$rootPath = $config->paths->root;
$targetPath = $this->cachePath;
@@ -1202,4 +1203,3 @@ class FileCompiler extends Wire {
}
}

View File

@@ -37,12 +37,12 @@ function wire($name = 'wire') {
*
* #pw-group-common
*
* @param ProcessWire|Wire|null $wire To set specify ProcessWire instance or any Wire-derived object in it, or omit to get current instance.
* @param Wire|null $wire To set specify ProcessWire instance or any Wire-derived object in it, or omit to get current instance.
* @return ProcessWire
* @since 3.0.125
*
*/
function wireInstance(Wire $wire = null) {
function wireInstance(?Wire $wire = null) {
if($wire === null) return ProcessWire::getCurrentInstance();
if(!$wire instanceof ProcessWire) $wire = $wire->wire();
ProcessWire::setCurrentInstance($wire);
@@ -592,39 +592,90 @@ function wireDate($format = '', $ts = null) {
/**
* Render markup for a system icon
*
*
* It is NOT necessary to specify an icon prefix like “fa-” with the icon name.
*
* Modifiers recognized in the class attribute:
* lg, fw, 2x, 3x, 4x, 5x, spin, spinner, li, border, inverse,
* rotate-90, rotate-180, rotate-270, flip-horizontal, flip-vertical,
* stack, stack-1x, stack-2x
*
* ~~~~~
* // Outputs: "<i class='fa fa-home'></i>"
* echo wireIconMarkup('home');
* echo wireIconMarkup('home');
*
* // Outputs: "<i class='fa fa-home fa-fw fa-lg my-class'></i>"
* echo wireIconMarkup('home', 'fw lg my-class');
*
* // Outputs "<i class='fa fa-home fa-fw' id='root-icon'></i>" (3.0.229+ only)
* echo wireIconMarkup('home', 'fw id=root-icon');
* echo wireIconMarkup('home fw id=root-icon'); // same as above
* ~~~~~
*
* #pw-group-markup
*
* @param string $icon Icon name (currently a font-awesome icon name, but support for more in future)
* @param string $class Additional attributes for class (example: "fw" for fixed width)
* @param string $icon Icon name (currently a font-awesome icon name)
* @param string $class Any of the following:
* - Additional attributes for class (example: "fw" for fixed width)
* - Your own custom class(es) separated by spaces
* - Any additional attributes in format `key="val" key='val' or key=val` string (3.0.229+)
* - An optional trailing space to append an `&nbsp;` to the return icon markup (3.0.229+)
* - Any of the above may also be specified in the $icon argument in 3.0.229+.
* @return string
*
*/
function wireIconMarkup($icon, $class = '') {
static $modifiers = null;
$sanitizer = wire()->sanitizer;
$attrs = array();
$append = '';
if($modifiers === null) $modifiers = array_flip(array(
'lg', 'fw', '2x', '3x', '4x', '5x',
'spin', 'spinner', 'li', 'border', 'inverse',
'rotate-90', 'rotate-180', 'rotate-270',
'flip-horizontal', 'flip-vertical',
'stack', 'stack-1x', 'stack-2x',
));
if(empty($icon)) return '';
if(strpos($icon, 'icon-') === 0) $icon = str_replace('icon-', 'fa-', $icon);
if(strpos($icon, 'fa-') !== 0) $icon = "fa-$icon";
if($class) {
$modifiers = array(
'lg', 'fw', '2x', '3x', '4x', '5x', 'spin', 'spinner', 'li', 'border',
'rotate-90', 'rotate-180', 'rotate-270', 'flip-horizontal', 'flip-vertical',
'stack', 'stack-1x', 'stack-2x', 'inverse',
);
$classes = explode(' ', $class);
foreach($classes as $key => $modifier) {
if(in_array($modifier, $modifiers)) $classes[$key] = "fa-$modifier";
if(strpos($icon, ' ')) {
// class or extras specified in $icon rather than $class
list($icon, $extra) = explode(' ', $icon, 2);
$class = trim("$class $extra");
}
if(strpos($icon, 'icon-') === 0) {
list(,$icon) = explode('icon-', $icon, 2);
$icon = "fa-$icon";
} else if(strpos($icon, 'fa-') !== 0) {
$icon = "fa-$icon";
}
if($class !== '') {
$classes = array();
if(rtrim($class) !== $class) $append = '&nbsp;';
if(strpos($class, '=')) {
$re = '/\b([-_a-z\d]+)=("[^"]*"|\'[^\']*\'|[-_a-z\d]+)\s*/i';
if(preg_match_all($re, $class, $matches)) {
foreach($matches[1] as $key => $attrName) {
$attrVal = trim($matches[2][$key], "\"'");
$attrVal = $sanitizer->entities($attrVal);
$attrs[$attrName] = "$attrName='$attrVal'";
$class = str_replace($matches[0][$key], ' ', $class);
}
$class = trim($class);
}
}
if(isset($attrs['class'])) {
$class = trim("$class $attrs[class]");
unset($attrs['class']);
}
foreach(explode(' ', $class) as $c) {
if(empty($c)) continue;
$classes[] = isset($modifiers[$c]) ? "fa-$c" : $c;
}
$class = implode(' ', $classes);
}
$class = trim("fa $icon $class");
return "<i class='$class'></i>";
$class = $sanitizer->entities(trim("fa $icon $class"));
$attrs['class'] = "class='$class'";
return "<i " . implode(' ', $attrs) . "></i>$append";
}
/**
@@ -1392,5 +1443,3 @@ function PageArray($items = array()) {
$pa = PageArray::newInstance($items);
return $pa;
}

View File

@@ -95,6 +95,7 @@ class ImageSizer extends Wire {
*
*/
public function __construct($filename = '', $options = array()) {
parent::__construct();
if(!empty($options)) $this->setOptions($options);
if(!empty($filename)) $this->setFilename($filename);
}
@@ -114,7 +115,7 @@ class ImageSizer extends Wire {
self::$knownEngines = array();
$modules = $this->wire('modules');
$modules = $this->wire()->modules;
$engines = $modules->findByPrefix('ImageSizerEngine');
$numEngines = count($engines);
@@ -228,7 +229,6 @@ class ImageSizer extends Wire {
$e = $this->getEngine($engineName);
if(!$e) continue;
/** @var ImageSizerEngine $e */
$e->prepare($filename, $options, $inspectionResult);
$supported = $e->supported();
@@ -430,7 +430,7 @@ class ImageSizer extends Wire {
$engineClass = __NAMESPACE__ . "\\$engineName";
$engine = $this->wire(new $engineClass());
} else {
$engine = $this->wire('modules')->get($engineName);
$engine = $this->wire()->modules->get($engineName);
}
return $engine;
}
@@ -452,7 +452,7 @@ class ImageSizer extends Wire {
return $this->engine;
}
public function __get($key) { return $this->getEngine()->__get($key); }
public function __get($name) { return $this->getEngine()->__get($name); }
/**
* ImageInformation from Image Inspector in short form or full RawInfoData
@@ -607,7 +607,7 @@ class ImageSizer extends Wire {
$count = 0;
while(!feof($fh) && $count < 2) {
$chunk = fread($fh, 1024 * 100); //read 100kb at a time
$count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk, $matches);
$count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk);
}
fclose($fh);
return $count > 1;
@@ -618,7 +618,7 @@ class ImageSizer extends Wire {
*
* @param mixed $image Pageimage or filename
*
* @return mixed|null|bool
* @return null|bool
*
*/
static public function imageResetIPTC($image) {

View File

@@ -399,7 +399,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
$this->inspectionResult = $inspectionResult;
// filling all options with global custom values from config.php
$options = array_merge($this->wire('config')->imageSizerOptions, $options);
$options = array_merge($this->wire()->config->imageSizerOptions, $options);
$this->setOptions($options);
$this->loadImageInfo($filename, false);
}
@@ -521,7 +521,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
if(is_callable("$className::getModuleInfo")) {
$moduleInfo = $className::getModuleInfo();
} else {
$moduleInfo = $this->wire('modules')->getModuleInfoVerbose($className);
$moduleInfo = $this->wire()->modules->getModuleInfoVerbose($className);
}
if(!is_array($moduleInfo)) $moduleInfo = array();
@@ -635,7 +635,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
*/
public function writeBackIPTC($filename, $includeCustomTags = false) {
if($this->wire('config')->debug) {
if($this->wire()->config->debug) {
// add a timestamp and the name of the image sizer engine to the IPTC tag number 217
$entry = $this->className() . '-' . date('Ymd:His');
if(!$this->iptcRaw) $this->iptcRaw = array();
@@ -648,9 +648,10 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
$dest = preg_replace('/\.' . $extension . '$/', '_tmp.' . $extension, $filename);
if(strlen($content) == @file_put_contents($dest, $content, \LOCK_EX)) {
// on success we replace the file
$this->wire('files')->unlink($filename);
$this->wire('files')->rename($dest, $filename);
$this->wire('files')->chmod($filename);
$files = $this->wire()->files;
$files->unlink($filename);
$files->rename($dest, $filename);
$files->chmod($filename);
return true;
} else {
// it was created a temp diskfile but not with all data in it
@@ -1460,7 +1461,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
/**
* Return the image type constant
*
* @return string
* @return string|null
*
*/
public function getImageType() {
@@ -1730,15 +1731,17 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
$this->finalWidth, $this->finalHeight)) {
return false; // fallback or failed
}
$files = $this->wire()->files;
if($this->webpOnly) {
$this->wire('files')->unlink($this->tmpFile);
$files->unlink($this->tmpFile);
} else {
// all went well, copy back the temp file,
if(!@copy($this->tmpFile, $this->filename)) return false; // fallback or failed
$this->wire('files')->chmod($this->filename);
$files->chmod($this->filename);
// remove the temp file
$this->wire('files')->unlink($this->tmpFile);
$files->unlink($this->tmpFile);
// post processing: IPTC, setModified and reload ImageInfo
$this->writeBackIPTC($this->filename, false);
}
@@ -1758,6 +1761,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*/
public function rotate($degrees, $dstFilename = '') {
$files = $this->wire()->files;
$degrees = (int) $degrees;
$srcFilename = $this->filename;
@@ -1767,7 +1771,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
if($degrees < -360) $degrees = $degrees - 360;
if($degrees == 0 || $degrees == 360 || $degrees == -360) {
if($dstFilename != $this->filename) wireCopy($this->filename, $dstFilename);
if($dstFilename != $this->filename) $files->copy($this->filename, $dstFilename);
return true;
}
@@ -1787,13 +1791,13 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
if($result) {
// success
if($tmpFilename != $dstFilename) {
if(is_file($dstFilename)) $this->wire('files')->unlink($dstFilename);
$this->wire('files')->rename($tmpFilename, $dstFilename);
if(is_file($dstFilename)) $files->unlink($dstFilename);
$files->rename($tmpFilename, $dstFilename);
}
$this->wire('files')->chmod($dstFilename);
$files->chmod($dstFilename);
} else {
// fail
if(is_file($tmpFilename)) $this->wire('files')->unlink($tmpFilename);
if(is_file($tmpFilename)) $files->unlink($tmpFilename);
}
return $result;
@@ -2076,7 +2080,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*/
public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
$f = $this->wire('modules')->get('InputfieldInteger');
$f = $inputfields->InputfieldInteger;
$f->attr('name', 'enginePriority');
$f->label = $this->_('Engine priority');
$f->description = $this->_('This determines what order this engine is tried in relation to other ImageSizerEngine modules.');
@@ -2086,7 +2090,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
$f->icon = 'sort-numeric-asc';
$inputfields->add($f);
$f = $this->wire('modules')->get('InputfieldRadios');
$f = $inputfields->InputfieldRadios;
$f->attr('name', 'sharpening');
$f->label = $this->_('Sharpening');
$f->addOption('none', $this->_('None'));
@@ -2098,7 +2102,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
$f->icon = 'image';
$inputfields->add($f);
$f = $this->wire('modules')->get('InputfieldInteger');
$f = $inputfields->InputfieldInteger;
$f->attr('name', 'quality');
$f->label = $this->_('Quality');
$f->description = $this->_('Default quality setting from 1 to 100 where 1 is lowest quality, and 100 is highest.');

View File

@@ -377,7 +377,7 @@ class ImageSizerEngineGD extends ImageSizerEngine {
}
// write to file(s)
if(file_exists($dstFilename)) $this->wire('files')->unlink($dstFilename);
if(file_exists($dstFilename)) $this->wire()->files->unlink($dstFilename);
$result = null; // null=not yet known
@@ -457,7 +457,7 @@ class ImageSizerEngineGD extends ImageSizerEngine {
if(!function_exists('imagewebp')) return false;
$path_parts = pathinfo($filename);
$webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp';
if(file_exists($webpFilename)) $this->wire('files')->unlink($webpFilename);
if(file_exists($webpFilename)) $this->wire()->files->unlink($webpFilename);
return imagewebp($im, $webpFilename, $quality);
}

View File

@@ -3,7 +3,7 @@
/**
* ProcessWire Inputfield - base class for Inputfield modules.
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* An Inputfield for an actual form input field widget, and this is provided as the base class
@@ -62,6 +62,7 @@
* @property string $tabLabel Label for tab if Inputfield rendered in its own tab via Inputfield::collapsedTab* setting. @since 3.0.201 #pw-group-labels
* @property string|null $prependMarkup Optional markup to prepend to the Inputfield content container. #pw-group-other
* @property string|null $appendMarkup Optional markup to append to the Inputfield content container. #pw-group-other
* @property string|null $footerMarkup Optional markup to add to the '.Inputfield' container, after '.InputfieldContent'. @since 3.0.241 #pw-advanced
*
* @method string|Inputfield label($label = null) Get or set the 'label' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield description($description = null) Get or set the 'description' property via method. @since 3.0.110 #pw-group-labels
@@ -102,16 +103,16 @@
* @property null|bool|Fieldtype $hasFieldtype The Fieldtype using this Inputfield, or boolean false when known not to have a Fieldtype, or null when not known. #pw-group-other
* @property null|Field $hasField The Field object associated with this Inputfield, or null when not applicable or not known. #pw-group-other
* @property null|Page $hasPage The Page object associated with this Inputfield, or null when not applicable or not known. #pw-group-other
* @property null|Inputfield $hasInputfield If this Inputfield is owned/managed by another (other than parent/child relationship), it may be set here. 3.0.176+ #pw-group-other
* @property bool|null $useLanguages When multi-language support active, can be set to true to make it provide inputs for each language, where supported (default=false). #pw-group-behavior
* @property null|Inputfield $hasInputfield If this Inputfield is owned/managed by another (other than parent/child relationship), it may be set here. @since 3.0.176 #pw-group-other
* @property bool|null $useLanguages When multi-language support active, can be set to true to make it provide inputs for each language, where supported (default=false). #pw-group-behavior #pw-group-languages
* @property null|bool|int $entityEncodeLabel Set to boolean false to specifically disable entity encoding of field header/label (default=true). #pw-group-output
* @property null|bool $entityEncodeText Set to boolean false to specifically disable entity encoding for other text: description, notes, etc. (default=true). #pw-group-output
* @property int $renderFlags Options that can be applied to render, see "render*" constants (default=0). #pw-group-output 3.0.204+
* @property int $renderFlags Options that can be applied to render, see "render*" constants (default=0). @since 3.0.204 #pw-group-output
* @property int $renderValueFlags Options that can be applied to renderValue mode, see "renderValue" constants (default=0). #pw-group-output
* @property string $wrapClass Optional class name (CSS) to apply to the HTML element wrapping the Inputfield. #pw-group-other
* @property string $headerClass Optional class name (CSS) to apply to the InputfieldHeader element #pw-group-other
* @property string $contentClass Optional class name (CSS) to apply to the InputfieldContent element #pw-group-other
* @property string $addClass Formatted class string letting you add class to any of the above (see addClass method). #pw-group-other 3.0.204+
* @property string $addClass Formatted class string letting you add class to any of the above (see addClass method). @since 3.0.204 #pw-group-other
* @property int|null $textFormat Text format to use for description/notes text in Inputfield (see textFormat constants) #pw-group-output
*
* @method string|Inputfield required($required = null) Get or set required state. @since 3.0.110 #pw-group-behavior
@@ -121,7 +122,11 @@
* @method string|Inputfield headerClass($class = null) Get header class attribute or add a class to it. @since 3.0.110 #pw-group-other
* @method string|Inputfield contentClass($class = null) Get content class attribute or add a class to it. @since 3.0.110 #pw-group-other
*
*
* MULTI-LANGUAGE METHODS (requires LanguageSupport module to be installed)
* ======================
* @method void setLanguageValue($language, $value) Set language value for Inputfield that supports it. Requires LanguageSupport module. $language can be Language, id (int) or name (string). @since 3.0.238 #pw-group-languages
* @method string|mixed getLanguageValue($language) Get language value for Inputfield that supports it. Requires LanguageSupport module. $language can be Language, id (int) or name (string). @since 3.0.238 #pw-group-languages
*
* HOOKABLE METHODS
* ================
* @method string render()
@@ -393,6 +398,14 @@ abstract class Inputfield extends WireData implements Module {
*/
protected $editable = true;
/**
* Header icon definitions
*
* @var array
*
*/
protected $headerActions = array();
/**
* Construct the Inputfield, setting defaults for all properties
*
@@ -421,8 +434,9 @@ abstract class Inputfield extends WireData implements Module {
$this->set('textFormat', self::textFormatBasic); // format applied to description and notes
$this->set('renderFlags', 0); // See render* constants
$this->set('renderValueFlags', 0); // see renderValue* constants, applicable to renderValue mode only
$this->set('prependMarkup', ''); // markup to prepend to Inputfield output
$this->set('appendMarkup', ''); // markup to append to Inputfield output
$this->set('prependMarkup', ''); // markup to prepend to InputfieldContent output
$this->set('appendMarkup', ''); // markup to append to InputfieldContent output
$this->set('footerMarkup', ''); // markup to add to end of Inputfield output
// default ID attribute if no 'id' attribute set
$this->defaultID = $this->className() . self::$numInstances;
@@ -505,7 +519,7 @@ abstract class Inputfield extends WireData implements Module {
if($value === true) $value = self::collapsedYes;
$value = (int) $value;
} else if(array_key_exists($key, $this->attributes)) {
} else if(array_key_exists($key, $this->attributes) && $key !== 'required') {
return $this->setAttribute($key, $value);
} else if($key === 'required' && $value && !is_object($value)) {
@@ -936,7 +950,7 @@ abstract class Inputfield extends WireData implements Module {
*/
protected function ___callUnknown($method, $arguments) {
$arg = isset($arguments[0]) ? $arguments[0] : null;
if(isset($this->attributes[$method])) {
if(isset($this->attributes[$method]) && $method !== 'required') {
// get or set an attribute
return $arg === null ? $this->getAttribute($method) : $this->setAttribute($method, $arg);
} else if(($value = $this->getSetting($method)) !== null) {
@@ -1356,11 +1370,11 @@ abstract class Inputfield extends WireData implements Module {
*
* #pw-internal
*
* @param array $attributes Associative array of attributes to build the string from, or omit to use this Inputfield's attributes.
* @param array|null $attributes Associative array of attributes to build the string from, or omit to use this Inputfield's attributes.
* @return string
*
*/
public function getAttributesString(array $attributes = null) {
public function getAttributesString(?array $attributes = null) {
$str = '';
@@ -1392,7 +1406,7 @@ abstract class Inputfield extends WireData implements Module {
continue;
}
$str .= "$attr=\"" . htmlspecialchars($value, ENT_QUOTES, "UTF-8") . '" ';
$str .= "$attr=\"" . htmlspecialchars("$value", ENT_QUOTES, "UTF-8") . '" ';
}
return trim($str);
@@ -1449,13 +1463,17 @@ abstract class Inputfield extends WireData implements Module {
*
* #pw-group-output
*
* @param Inputfield|InputfieldWrapper|null The parent InputfieldWrapper that is rendering it, or null if no parent.
* @param Inputfield|null The parent InputfieldWrapper that is rendering it, or null if no parent.
* @param bool $renderValueMode Specify true only if this is for `Inputfield::renderValue()` rather than `Inputfield::render()`.
* @return bool True if assets were just added, false if already added.
*
*/
public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
$result = $this->wire()->modules->loadModuleFileAssets($this) > 0;
public function renderReady(?Inputfield $parent = null, $renderValueMode = false) {
if($this->className() === 'InputfieldWrapper') {
$result = false;
} else {
$result = $this->wire()->modules->loadModuleFileAssets($this) > 0;
}
if($this->wire()->hooks->isMethodHooked($this, 'renderReadyHook')) {
$this->renderReadyHook($parent, $renderValueMode);
}
@@ -1467,11 +1485,11 @@ abstract class Inputfield extends WireData implements Module {
*
* Hook this method instead if you want to hook renderReady().
*
* @param Inputfield $parent
* @param Inputfield|null $parent
* @param bool $renderValueMode
*
*/
public function ___renderReadyHook(Inputfield $parent = null, $renderValueMode = false) { }
public function ___renderReadyHook(?Inputfield $parent = null, $renderValueMode = false) { }
/**
* This hook was replaced by renderReady
@@ -2077,6 +2095,86 @@ abstract class Inputfield extends WireData implements Module {
return $this->editable;
}
/**
* Add header action
*
* This adds a clickable icon to the right side of the Inputfield header.
* There are three types of actions: 'click', 'toggle' and 'link'. The 'click'
* action simply triggers your JS event whenever it is clicked. The 'toggle' action
* has an on/off state, and you can specify the JS event to trigger for each.
* This function will automatically figure out whether you want a `click`,
* `toggle` or 'link' action based on what you provide in the $settings argument.
* Below is a summary of these settings:
*
* Settings for 'click' or 'link' type actions
* -------------------------------------------
* - `icon` (string): Name of font-awesome icon to use.
* - `tooltip` (string): Optional tooltip text to display when icon hovered.
* - `event` (string): Event name to trigger in JS when clicked ('click' actions only).
* - `href` (string): URL to open ('link' actions only).
* - `modal` (bool): Specify true to open link in modal ('link' actions only).
*
* Settings for 'toggle' (on/off) type actions
* -------------------------------------------
* - `on` (bool): Start with the 'on' state? (default=false)
* - `onIcon` (string): Name of font-awesome icon to show for on state.
* - `onEvent` (string): JS event name to trigger when toggled on.
* - `onTooltip` (string): Tooltip text to show when on icon is hovered.
* - `offIcon` (string): Name of font-awesome icon to show for off state.
* - `offEvent` (string): JS event name to trigger when toggled off.
* - `offTooltip` (string): Tooltip text to show when off icon is hovered.
*
* Other/optional settings (applies to all types)
* ----------------------------------------------
* - `name` (string): Name of this action (-_a-zA-Z0-9).
* - `parent` (string): Name of parent action, if this action is part of a menu.
* - `overIcon` (string): Name of font-awesome icon to show when hovered.
* - `overEvent` (string): JS event name to trigger when mouse is over the icon.
* - `downIcon` (string): Icon to display when mouse is down on the action icon (3.0.241+).
* - `downEvent` (string): JS event name to trigger when mouse is down on the icon (3.0.241+).
* - `cursor` (string): CSS cursor name to show when mouse is over the icon.
* - `setAll` (array): Set all of the header actions in one call, replaces any existing.
* Note: to get all actions, call the method and omit the $settings argument.
*
* Settings for dropdown menu actions (3.0.241+)
* ---------------------------------------------
* Note that menu type actions also require jQuery UI and /wire/templates-admin/scripts/main.js,
* both of which are already present in PWs admin themes (AdminThemeUikit recommended).
* Requires ProcessWire 3.0.241 or newer.
* - `icon` (string): Icon name to use for dropdown toggle, i.e. 'fa-wrench'.
* - `tooltip` (string): Optional tooltip to describe what the dropdown is for.
* - `menuAction` (string): Action that toggles the menu to show, one of 'click' or 'hover' (default).
* - `menuItems` (array): Definition of menu items, each with one or more of the following properties.
* - `label` (string): Label text for the menu item (required).
* - `icon` (string): Icon name for the menu item, if desired.
* - `callback` (function|null): JS callback to execute item is clicked (not applicable in PHP).*
* - `event` (string): JS event name to trigger when item is clicked.
* - `tooltip` (string): Tooltip text to show when hovering menu item (title attribute).
* - `href` (string): URL to go to when menu item clicked.
* - `target` (string): Target attribute when href is used (i.e. "_blank").
* - `modal` (bool): Open href in modal window instead?
* - `active` (function|bool): Callback function that returns true if menu item active, or false.*
* if disabled. You can also directly specify true or false for this option.
* - NOTE 1: All `menuItems` properties above are optional, except for 'label'.
* - NOTE 2: To use `callback` or `active` as functions, you must define your menu in JS instead.
* - NOTE 3: For examples see the addHeaderAction() method in /wire/templates-admin/scripts/inputfields.js
*
* @param array $settings Specify array containing the appropriate settings above.
* @return array Returns all currently added actions.
* @since 3.0.240
*
*/
public function addHeaderAction(array $settings = array()) {
if(!empty($settings['setAll'])) {
if(is_array($settings['setAll'])) {
$this->headerActions = array_values($settings['setAll']);
}
} else {
$this->headerActions[] = $settings; // add new action
}
return $this->headerActions; // return all
}
/**
* debugInfo PHP 5.6+ magic method
*

View File

@@ -3,7 +3,7 @@
/**
* ProcessWire InputfieldWrapper
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* About InputfieldWrapper
@@ -15,6 +15,8 @@
*
* InputfieldWrapper is not designed to render an Inputfield specifically, but you can set a value attribute
* containing content that will be rendered before the wrapper.
*
* #pw-summary-properties Access any common Inputfield type class name from an InputfieldWrapper and it will return a new instance of that Inputfield, i.e. `$f = $inputfields->InputfieldText;` Below are several examples.
*
* @property bool $renderValueMode True when only rendering values, i.e. no inputs (default=false). #pw-internal
* @property bool $quietMode True to suppress label, description and notes, often combined with renderValueMode (default=false). #pw-internal
@@ -27,47 +29,47 @@
* @method Inputfield new($typeName, $name = '', $label = '', array $settings = []) #pw-group-manipulation
* @method bool allowProcessInput(Inputfield $inputfield) Allow Inputfield to have input processed? (3.0.207+) #pw-internal
*
* @property InputfieldAsmSelect $InputfieldAsmSelect
* @property InputfieldButton $InputfieldButton
* @property InputfieldCheckbox $InputfieldCheckbox
* @property InputfieldCheckboxes $InputfieldCheckboxes
* @property InputfieldCKEditor $InputfieldCkeditor
* @property InputfieldCommentsAdmin $InputfieldCommentsAdmin
* @property InputfieldDatetime $InputfieldDatetime
* @property InputfieldEmail $InputfieldEmail
* @property InputfieldFieldset $InputfieldFieldset
* @property InputfieldFieldsetClose $InputfieldlFieldsetClose
* @property InputfieldFieldsetOpen $InputfieldFieldsetOpen
* @property InputfieldFieldsetTabOpen $InputfieldFieldsetTabOpen
* @property InputfieldFile $InputfieldFile
* @property InputfieldFloat $InputfieldFloat
* @property InputfieldForm $InputfieldForm
* @property InputfieldHidden $InputfieldHidden
* @property InputfieldIcon $InputfieldIcon
* @property InputfieldImage $InputfieldImage
* @property InputfieldInteger $InputfieldInteger
* @property InputfieldMarkup $InputfieldMarkup
* @property InputfieldName $InputfieldName
* @property InputfieldPage $InputfieldPage
* @property InputfieldPageAutocomplete $InputfieldPageAutocomplete
* @property InputfieldPageListSelect $InputfieldPageListSelect
* @property InputfieldPageListSelectMultiple $InputfieldPageListSelectMultiple
* @property InputfieldPageName $InputfieldPageName
* @property InputfieldPageTable $InputfieldPageTable
* @property InputfieldPageTitle $InputfieldPageTitle
* @property InputfieldPassword $InputfieldPassword
* @property InputfieldRadios $InputfieldRadios
* @property InputfieldRepeater $InputfieldRepeater
* @property InputfieldSelect $InputfieldSelect
* @property InputfieldSelectMultiple $InputfieldSelectMultiple
* @property InputfieldSelector $InputfieldSelector
* @property InputfieldSubmit $InputfieldSubmit
* @property InputfieldText $InputfieldText
* @property InputfieldTextarea $InputfieldTextarea
* @property InputfieldTextTags $InputfieldTextTags
* @property InputfieldToggle $InputfieldToggle
* @property InputfieldURL $InputfieldURL
* @property InputfieldWrapper $InputfieldWrapper
* @property InputfieldAsmSelect $InputfieldAsmSelect Create new asmSelect Inputfield #pw-group-properties
* @property InputfieldButton $InputfieldButton Create new button Inputfield #pw-group-properties
* @property InputfieldCheckbox $InputfieldCheckbox Create new checkbox Inputfield #pw-group-properties
* @property InputfieldCheckboxes $InputfieldCheckboxes Create new checkboxes Inputfield #pw-group-properties
* @property InputfieldCKEditor $InputfieldCKEditor Create new CKEditor Inputfield #pw-group-properties
* @property InputfieldCommentsAdmin $InputfieldCommentsAdmin #pw-internal
* @property InputfieldDatetime $InputfieldDatetime Create new date/time Inputfield #pw-group-properties
* @property InputfieldEmail $InputfieldEmail Create new email Inputfield #pw-group-properties
* @property InputfieldFieldset $InputfieldFieldset Create new Fieldset InputfieldWrapper #pw-group-properties
* @property InputfieldFieldsetClose $InputfieldlFieldsetClose #pw-internal
* @property InputfieldFieldsetOpen $InputfieldFieldsetOpen #pw-internal
* @property InputfieldFieldsetTabOpen $InputfieldFieldsetTabOpen #pw-internal
* @property InputfieldFile $InputfieldFile Create new file Inputfield #pw-group-properties
* @property InputfieldFloat $InputfieldFloat Create new float Inputfield #pw-group-properties
* @property InputfieldForm $InputfieldForm Create new form InputfieldWrapper #pw-group-properties
* @property InputfieldHidden $InputfieldHidden Create new hidden Inputfield #pw-group-properties
* @property InputfieldIcon $InputfieldIcon Create new icon Inputfield #pw-group-properties
* @property InputfieldImage $InputfieldImage Create new image Inputfield #pw-group-properties
* @property InputfieldInteger $InputfieldInteger Create new integer Inputfield #pw-group-properties
* @property InputfieldMarkup $InputfieldMarkup Create new markup Inputfield #pw-group-properties
* @property InputfieldName $InputfieldName #pw-internal
* @property InputfieldPage $InputfieldPage Create new Page selection Inputfield #pw-group-properties
* @property InputfieldPageAutocomplete $InputfieldPageAutocomplete Create new Page selection autocomplete Inputfield #pw-group-properties
* @property InputfieldPageListSelect $InputfieldPageListSelect Create new PageListSelect Inputfield #pw-group-properties
* @property InputfieldPageListSelectMultiple $InputfieldPageListSelectMultiple Create new multiple PageListSelect Inputfield #pw-group-properties
* @property InputfieldPageName $InputfieldPageName #pw-internal
* @property InputfieldPageTable $InputfieldPageTable #pw-internal
* @property InputfieldPageTitle $InputfieldPageTitle #pw-internal
* @property InputfieldPassword $InputfieldPassword #pw-internal
* @property InputfieldRadios $InputfieldRadios Create new radio buttons Inputfield #pw-group-properties
* @property InputfieldRepeater $InputfieldRepeater #pw-internal
* @property InputfieldSelect $InputfieldSelect Create new <select> Inputfield #pw-group-properties
* @property InputfieldSelectMultiple $InputfieldSelectMultiple Create new <select multiple> Inputfield #pw-group-properties
* @property InputfieldSelector $InputfieldSelector #pw-internal
* @property InputfieldSubmit $InputfieldSubmit Create new submit button Inputfield #pw-group-properties
* @property InputfieldText $InputfieldText Create new single-line text Inputfield #pw-group-properties
* @property InputfieldTextarea $InputfieldTextarea Create new multi-line <textarea> Inputfield #pw-group-properties
* @property InputfieldTextTags $InputfieldTextTags Create new text tags Inputfield #pw-group-properties
* @property InputfieldToggle $InputfieldToggle Create new toggle Inputfield #pw-group-properties
* @property InputfieldURL $InputfieldURL Create new URL Inputfield #pw-group-properties
* @property InputfieldWrapper $InputfieldWrapper Create new generic InputfieldWrapper #pw-group-properties
*
*/
@@ -89,7 +91,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
'list' => "<ul {attrs}>{out}</ul>",
'item' => "<li {attrs}>{out}</li>",
'item_label' => "<label class='InputfieldHeader ui-widget-header{class}' for='{for}'>{out}</label>",
'item_label_hidden' => "<label class='InputfieldHeader InputfieldHeaderHidden ui-widget-header{class}'><span>{out}</span></label>",
'item_label_hidden' => "<label class='InputfieldHeader InputfieldHeaderHidden ui-widget-header{class}' for='{for}'><span>{out}</span></label>",
'item_content' => "<div class='InputfieldContent ui-widget-content{class}'>{out}</div>",
'item_error' => "<p class='InputfieldError ui-state-error'><i class='fa fa-fw fa-flash'></i><span>{out}</span></p>",
'item_description' => "<p class='description'>{out}</p>",
@@ -402,6 +404,8 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
/**
* Insert new or existing Inputfield before or after another
*
* #pw-group-manipulation
*
* @param Inputfield|array|string $item New or existing item Inputfield, name, or new item array to insert.
* @param Inputfield|string $existingItem Existing item or item name you want to insert before.
* @param bool $before True to insert before, false to insert after (default=false).
@@ -555,6 +559,27 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
return $this;
}
/**
* Remove an Inputfield from the form by name
*
* Note that this works the same as the getByName/getChildByName methods in that it
* will find (and remove) the field by name, even if nested within other wrappers
* or fieldsets. It returns the removed Inputfield when found, or null if not.
*
* @param string $name
* @return Inputfield|null Removed Inputfield object on success, or null if not found
* @since 3.0.250
*
*/
public function removeByName($name) {
$f = $this->getByName((string) $name);
if(!$f) return null;
$parent = $f->getParent();
if(!$parent instanceof InputfieldWrapper) return null;
$parent->remove($f);
return $f;
}
/**
* Prepare children for rendering by creating any fieldset groups
*
@@ -721,6 +746,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$classes = array();
$useColumnWidth = $this->useColumnWidth;
$renderAjaxInputfield = $this->wire()->config->ajax ? $this->wire()->input->get('renderInputfieldAjax') : null;
$toggleLabel = $sanitizer->entities1($this->_('Toggle open/close'));
$lockedStates = array(
Inputfield::collapsedNoLocked,
@@ -766,7 +792,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
if(in_array($collapsed, $lockedStates)) $renderValueMode = true;
$ffOut = $this->renderInputfield($inputfield, $renderValueMode);
if(!strlen($ffOut)) continue;
if(!strlen("$ffOut")) continue;
$collapsed = (int) $inputfield->getSetting('collapsed'); // retrieve again after render
$entityEncodeText = $inputfield->getSetting('entityEncodeText') === false ? false : true;
@@ -883,25 +909,29 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$icon = $icon ? str_replace('{name}', $sanitizer->name(str_replace(array('icon-', 'fa-'), '', $icon)), $markup['item_icon']) : '';
$toggle = $collapsed == Inputfield::collapsedNever ? '' : $markup['item_toggle'];
if($toggle && strpos($toggle, 'title=') === false) {
$toggle = str_replace("class=", "title='" . $this->_('Toggle open/close') . "' class=", $toggle);
$toggle = str_replace("class=", "title='$toggleLabel' class=", $toggle);
}
$headerActions = $inputfield->addHeaderAction();
if(count($headerActions)) {
$label .= $this->renderHeaderActions($inputfield, $headerActions);
}
if($skipLabel === Inputfield::skipLabelHeader || $quietMode) {
// label only shows when field is collapsed
$label = str_replace('{out}', $icon . $label . $toggle, $markup['item_label_hidden']);
$labelHidden = $markup['item_label_hidden'];
if(strpos($labelHidden, '{for}')) $labelHidden = str_replace('{for}', $inputfield->attr('id'), $labelHidden);
$label = str_replace('{out}', $icon . $label . $toggle, $labelHidden);
} else {
// label always visible
$label = str_replace(array('{for}', '{out}'), array($for, $icon . $label . $toggle), $markup['item_label']);
$label = str_replace('{out}', $icon . $label . $toggle, $markup['item_label']);
if($skipLabel === Inputfield::skipLabelFor) {
$label = $this->removeAttributeFromMarkup('for', $label);
} else {
$label = $this->setAttributeInMarkup('for', $for, $label, true);
}
}
$headerClass = trim($inputfield->getSetting('headerClass') . " $classes[item_label]");
if($headerClass) {
if(strpos($label, '{class}') !== false) {
$label = str_replace('{class}', ' ' . $headerClass, $label);
} else {
$label = preg_replace('/( class=[\'"][^\'"]+)/', '$1 ' . $headerClass, $label, 1);
}
} else if(strpos($label, '{class}') !== false) {
$label = str_replace('{class}', '', $label);
}
$label = $this->setAttributeInMarkup('class', $headerClass, $label);
} else if($skipLabel === Inputfield::skipLabelMarkup) {
// no header and no markup for header
$label = '';
@@ -960,16 +990,9 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
}
$markupItemContent = $markup['item_content'];
$contentClass = trim($inputfield->getSetting('contentClass') . " $classes[item_content]");
if($contentClass) {
if(strpos($markupItemContent, '{class}') !== false) {
$markupItemContent = str_replace('{class}', ' ' . $contentClass, $markupItemContent);
} else {
$markupItemContent = preg_replace('/( class=[\'"][^\'"]+)/', '$1 ' . $contentClass, $markupItemContent, 1);
}
} else if(strpos($markupItemContent, '{class}') !== false) {
$markupItemContent = str_replace('{class}', '', $markupItemContent);
}
if($inputfield->className() != 'InputfieldWrapper') $ffOut = str_replace('{out}', $ffOut, $markupItemContent);
$markupItemContent = $this->setAttributeInMarkup('class', $contentClass, $markupItemContent);
if($inputfield->className() != 'InputfieldWrapper') $ffOut = str_replace('{out}', $ffOut, $markupItemContent);
$ffOut .= $inputfield->getSetting('footerMarkup');
$out .= str_replace(array('{attrs}', '{out}'), array(trim($attrs), $label . $ffOut), $markup['item']);
$lastInputfield = $inputfield;
} // foreach($children as $inputfield)
@@ -992,6 +1015,129 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
return $out;
}
/**
* Set attribute value in markup, optionally replacing a {placeholder} tag
*
* When a placeholder is present in the given $markup, it should be the
* attribute name wrapped in `{}`, i.e. `{class}`
*
* Note that class attributes are appended while other attributes are replaced.
*
* @param string $name Attribute name (i.e. "class", "for", etc.)
* @param string $value Value to set for the attribute
* @param string $markup Markup where the attribute or placeholder exists
* @param bool $removeEmpty Remove attribute if it resolves to empty value?
* @return string Updated markup
* @since 3.0.242
*
*/
protected function setAttributeInMarkup($name, $value, $markup, $removeEmpty = false) {
$placeholder = '{' . $name . '}';
$hasPlaceholder = strpos($markup, $placeholder) !== false;
if(strlen("$value")) {
if($hasPlaceholder) {
// replace existing class="… with class="… value
$replacement = $name === 'class' ? " $value" : $value;
$markup = str_replace($placeholder, $replacement, $markup);
} else if(strpos($markup, " $name=") !== false) {
// update existing attribute value without a {class} being present
// for class attribute it appends existing, for others it replaces
$replacement = $name === 'class' ? "$1 $value" : $value;
$markup = preg_replace('/(\s' . $name . '=[\'"][^\'"]*)/', $replacement, $markup, 1);
} else {
// insert attribute where it doesn't currently exist
$markup = preg_replace('!(<[a-z0-9]+)(\s*)!i', "$1 $name='$value'$2", $markup, 1);
}
// remove unnecessary whitespace in class attribute values
if($name === 'class') {
foreach(array(" $name=' ", " $name=\" ") as $find) {
while(strpos($markup, $find)) {
$markup = str_replace($find, rtrim($find), $markup);
}
}
}
} else if($hasPlaceholder) {
if($removeEmpty) {
// remove name="{name}"
$markup = str_replace(array(" $name='{" . $name . "}'", " $name=\"{" . $name . "}\""), '', $markup);
} else {
// replace {name} with blank string
$markup = str_replace($placeholder, '', $markup);
}
} else {
// $value is empty and there is no placeholder, leave $markup as-is
}
return $markup;
}
/**
* Remove named attribute from given markup
*
* @param string $name
* @param string $markup
* @return string
* @since 3.0.250
*
*/
protected function removeAttributeFromMarkup($name, $markup) {
if(stripos($markup, " $name=") === false) return $markup;
return preg_replace('!\s' . $name . '=["\'][^"\']*["\']!i', '', $markup);
}
/**
* Render Inputfield header actions
*
* @param Inputfield $inputfield
* @param array $actions
* @return string
* @since 3.0.240
*
*/
protected function renderHeaderActions(Inputfield $inputfield, array $actions) {
$sanitizer = $this->wire()->sanitizer;
$out = '';
$modal = false;
foreach($actions as $a) {
$icon = '';
$type = '';
if(isset($a['icon'])) {
$icon = $a['icon'];
if(isset($a['href'])) {
$type = 'link';
if(!empty($a['modal'])) $modal = true;
} else {
$type = 'click';
}
} else if(isset($a['offIcon'])) {
$type = 'toggle';
if(!isset($a['onIcon'])) $a['onIcon'] = $a['offIcon'];
} else if(isset($a['onIcon'])) {
$type = 'toggle';
$a['offIcon'] = $a['onIcon'];
}
if($type === 'toggle') $icon = !empty($a['on']) ? $a['onIcon'] : $a['offIcon'];
if(empty($icon) || empty($type)) continue;
$a['type'] = $type;
if(strpos($icon, 'fa-') !== 0) $icon = "fa-$icon";
$data = $sanitizer->entities(json_encode($a));
$out .= "<i class='_InputfieldHeaderAction fa fa-fw $icon' data-action='$data' hidden></i>";
}
if($modal) {
/** @var JqueryUI $jQueryUI */
$jQueryUI = $this->wire()->modules->get('JqueryUI');
$jQueryUI->use('modal');
}
return $out;
}
/**
* Render the output of this Inputfield and its children, showing values only (no inputs)
*
@@ -1132,7 +1278,22 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$url .= "renderInputfieldAjax=$inputfieldID";
$url = $sanitizer->entities($url);
$out = "<div class='renderInputfieldAjax'><input type='hidden' value='$url' /></div>";
$valueInput = '';
$val = $inputfield->val();
if(!is_array($val) && !is_object($val)) {
$val = (string) $val;
if(strlen("$val") <= 1024) {
// keep value in hidden input so dependences can refer to it
$val = $sanitizer->entities("$val");
$valueInput = "<input type='hidden' id='$inputfieldID' value='$val' />";
}
}
$out =
"<div class='renderInputfieldAjax'>" .
"<input type='hidden' value='$url' />" .
$valueInput .
"</div>";
if($inputfield instanceof InputfieldWrapper) {
// load assets they will need
@@ -1566,6 +1727,64 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
return $inputfield;
}
/**
* Get Inputfield by Field (hasField)
*
* This is useful in cases where the input name may differ from the Field name
* that it represents, and you only know the field name. Applies only to
* Inputfields connected with a Page and Field (i.e. used for page editing).
*
* #pw-group-retrieval-and-traversal
*
* @param Field|string|int $field
* @return Inputfield|InputfieldWrapper|null
* @since 3.0.239
*
*/
public function getByField($field) {
if(!$field instanceof Field) $field = $this->wire()->fields->get($field);
return $this->getByProperty('hasField', $field);
}
/**
* Get Inputfield by some other non-attribute property or setting
*
* #pw-group-retrieval-and-traversal
*
* @param string $property
* @param mixed $value
* @param bool $getAll Get array of all matching Inputfields rather than just first? (default=false)
* @return Inputfield|InputfieldWrapper|null|array
* @since 3.0.239
*
*/
public function getByProperty($property, $value, $getAll = false) {
$inputfield = null;
$value = (string) $value;
$a = array();
foreach($this->children() as $child) {
/** @var Inputfield $child */
if((string) $child->getSetting($property) === $value) {
$inputfield = $child;
} else if($child instanceof InputfieldWrapper) {
if($getAll) {
$a = array_merge($a, $child->getByProperty($property, $value, true));
} else {
$inputfield = $child->getByProperty($property, $value);
}
}
if($inputfield) {
if($getAll) {
$a[] = $inputfield;
} else {
break;
}
}
}
return $getAll ? $a : $inputfield;
}
/**
* Get value of Inputfield by name
*
@@ -1684,12 +1903,17 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
/** @var InputfieldSelect $f */
$f = $inputfields->getChildByName('collapsed');
if($f) {
// remove all options for 'collapsed' except for a few
// whitelist of collapsed options allowed for fieldsets/wrappers
$allow = array(
Inputfield::collapsedNo,
Inputfield::collapsedYes,
Inputfield::collapsedYesAjax,
Inputfield::collapsedNever,
Inputfield::collapsedHidden,
Inputfield::collapsedBlank,
Inputfield::collapsedPopulated,
Inputfield::collapsedBlankAjax,
Inputfield::collapsedBlankLocked,
);
foreach($f->getOptions() as $value => $label) {
if(!in_array($value, $allow)) $f->removeOption($value);
@@ -1797,11 +2021,11 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
* #pw-group-manipulation
*
* @param array $a Array of Inputfield definitions
* @param InputfieldWrapper $inputfields Specify the wrapper you want them added to, or omit to use current.
* @param InputfieldWrapper|null $inputfields Specify the wrapper you want them added to, or omit to use current.
* @return $this
*
*/
public function importArray(array $a, InputfieldWrapper $inputfields = null) {
public function importArray(array $a, ?InputfieldWrapper $inputfields = null) {
$modules = $this->wire()->modules;

View File

@@ -5,12 +5,18 @@
*
* The default numeric indexing of a WireArray is not overridden.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
*/
class InputfieldsArray extends WireArray {
public function __construct() {
parent::__construct();
$this->usesNumericKeys = true;
$this->indexedByName = false;
}
/**
* Per WireArray interface, only Inputfield instances are accepted.
@@ -31,6 +37,7 @@ class InputfieldsArray extends WireArray {
*
*/
public function find($selector) {
/** @var WireArray|InputfieldsArray $a */
$a = parent::find($selector);
foreach($this as $item) {
if(!$item instanceof InputfieldWrapper) continue;
@@ -44,8 +51,4 @@ class InputfieldsArray extends WireArray {
return null; // Inputfield is abstract, so there is nothing to return here
}
public function usesNumericKeys() {
return true;
}
}

View File

@@ -236,6 +236,58 @@ interface FieldtypeHasPageimages {
public function getPageimages(Page $page, Field $field);
}
/**
* Indicates Fieldtype has version support and manages its own versions
*
*/
interface FieldtypeDoesVersions {
/**
* Get the value for given page, field and version
*
* @param Page $page
* @param Field $field
* @param int $version
* @return mixed
*
*/
public function getPageFieldVersion(Page $page, Field $field, $version);
/**
* Save version of given page field
*
* @param Page $page
* @param Field $field
* @param int $version
* @return bool
*
*/
public function savePageFieldVersion(Page $page, Field $field, $version);
/**
* Restore version of given page field to live page
*
* @param Page $page
* @param Field $field
* @param int $version
* @return bool
*
*/
public function restorePageFieldVersion(Page $page, Field $field, $version);
/**
* Delete version
*
* @param Page $page
* @param Field $field
* @param int $version
* @return bool
*
*/
public function deletePageFieldVersion(Page $page, Field $field, $version);
}
/**
* Indicates that an Inputfield provides tree selection capabilities
*
@@ -706,7 +758,7 @@ interface InputfieldHasSelectableOptions {
* @return self|$this
*
*/
public function addOption($value, $label = null, array $attributes = null);
public function addOption($value, $label = null, ?array $attributes = null);
/**
* Add selectable option with label, optionally for specific language

View File

@@ -74,12 +74,12 @@ class MarkupFieldtype extends WireData implements Module {
* If you construct without providing page and field, please populate them
* separately with the setPage and setField methods before calling render().
*
* @param Page $page
* @param Field $field
* @param Page|null $page
* @param Field|null $field
* @param mixed $value
*
*/
public function __construct(Page $page = null, Field $field = null, $value = null) {
public function __construct(?Page $page = null, ?Field $field = null, $value = null) {
parent::__construct();
if($page) $this->setPage($page);
if($field) $this->setField($field);
@@ -266,11 +266,14 @@ class MarkupFieldtype extends WireData implements Module {
*
*/
protected function valueToString($value, $encode = true) {
if($value instanceof Pagefiles || $value instanceof Pagefile) {
$isObject = is_object($value);
if($isObject && ($value instanceof Pagefiles || $value instanceof Pagefile)) {
return $this->objectToString($value);
} else if($isObject && wireInstanceOf($value, 'RepeaterPageArray')) {
return $this->objectToString($value);
} else if(WireArray::iterable($value)) {
return $this->arrayToString($value);
} else if(is_object($value)) {
} else if($isObject) {
return $this->objectToString($value);
} else {
return $encode ? $this->wire()->sanitizer->entities1($value) : $value;
@@ -303,11 +306,16 @@ class MarkupFieldtype extends WireData implements Module {
*
*/
protected function objectToString($value) {
if($value instanceof WireArray) {
if($value instanceof WireArray) {
if(!$value->count()) return '';
if(wireInstanceOf($value, 'RepeaterPageArray')) {
return $this->renderInputfieldValue($value);
}
}
if($value instanceof Page) {
if($value->viewable()) {
if(wireInstanceOf($value, 'FieldsetPage')) {
return $this->renderInputfieldValue($value);
} else if($value->viewable()) {
return "<a href='$value->url'>" . $value->get('title|name') . "</a>";
} else {
return $value->get('title|name');
@@ -385,7 +393,7 @@ class MarkupFieldtype extends WireData implements Module {
*
*/
public function __toString() {
return $this->render();
return (string) $this->render();
}
public function setPage(Page $page) { $this->_page = $page; }

View File

@@ -67,11 +67,11 @@ class MarkupQA extends Wire {
/**
* Construct
*
* @param Page $page
* @param Field $field
* @param Page|null $page
* @param Field|null $field
*
*/
public function __construct(Page $page = null, Field $field = null) {
public function __construct(?Page $page = null, ?Field $field = null) {
parent::__construct();
if($page) {
$this->setPage($page);
@@ -509,7 +509,6 @@ class MarkupQA extends Wire {
$replacements = array();
$languages = $this->wire()->languages;
$config = $this->wire()->config;
$pages = $this->wire()->pages;
$rootURL = $config->urls->root;
$adminURL = $config->urls->admin;
$adminPath = $rootURL === '/' ? $adminURL : str_replace($rootURL, '/', $adminURL);
@@ -542,10 +541,8 @@ class MarkupQA extends Wire {
} else {
$language = null;
}
$livePath = $pages->getPath($pageID, array(
'language' => $language
));
$livePath = $this->getPagePathFromId($pageID, $language);
if($urlSegmentStr) {
$livePath = rtrim($livePath, '/') . "/$urlSegmentStr";
@@ -609,7 +606,7 @@ class MarkupQA extends Wire {
/**
* Find pages linking to another
*
* @param Page $page Page to find links to, or omit to use page specified in constructor
* @param Page|null $page Page to find links to, or omit to use page specified in constructor
* @param array $fieldNames Field names to look in or omit to use field specified in constructor
* @param string $selector Optional selector to use as a filter
* @param array $options Additional options
@@ -620,7 +617,7 @@ class MarkupQA extends Wire {
* @return PageArray|array|int
*
*/
public function findLinks(Page $page = null, $fieldNames = array(), $selector = '', array $options = array()) {
public function findLinks(?Page $page = null, $fieldNames = array(), $selector = '', array $options = array()) {
$pages = $this->wire()->pages;
$fields = $this->wire()->fields;
@@ -1026,4 +1023,67 @@ class MarkupQA extends Wire {
$this->settings['verbose'] = $verbose ? true : false;
}
/**
* Given page ID return the path to it
*
* @param int $pageID
* @param Language|null $language
* @return string
* @since 3.0.231
*
*/
protected function getPagePathFromId($pageID, $language = null) {
$pages = $this->wire()->pages;
$path = null;
if($this->isPagePathHooked()) {
$page = $pages->get($pageID);
if($page->id) {
if($language && $language->id) {
$languages = $this->wire()->languages;
$languages->setLanguage($language);
$path = $page->path();
$languages->unsetLanguage();
} else {
$path = $page->path();
}
}
}
if($path === null) {
$path = $pages->getPath($pageID, array(
'language' => $language
));
}
return $path;
}
/**
* Is the Page::path method hooked in a manner that might affect MarkupQA?
*
* @return bool
* @since 3.0.231
*
*/
protected function isPagePathHooked() {
$config = $this->wire()->config;
$property = '_MarkupQA_pagePathHooked';
$hooked = $config->get($property);
if($hooked !== null) return $hooked;
$hooks = $this->wire()->hooks;
$hooked = $hooks->isHooked('Page::path()');
if($hooked) {
// only consider Page::path hooked if something other than LanguageSupportPageNames hooks it
$hookItems = $hooks->getHooks($this->page, 'path', WireHooks::getHooksStatic);
foreach($hookItems as $key => $hook) {
if(((string) $hook['toObject']) === 'LanguageSupportPageNames') unset($hookItems[$key]);
}
$hooked = count($hookItems) > 0;
}
$config->setQuietly($property, $hooked);
return $hooked;
}
}

View File

@@ -270,7 +270,7 @@
* - Singular modules will have their instance active for the entire request after instantiated.
* - Non-singular modules return a new instance on every `$modules->get("YourModule")` call.
* - Modules that attach hooks are usually singular.
* - Modules that may have multiple instances (like `Inputfield` modules) should _not_be singular.
* - Modules that may have multiple instances (like `Inputfield` modules) should _not_ be singular.
*
* If you are having trouble deciding whether to make your module singular or not, be sure to read
* the documentation below for the `isAutoload()` method, because if your module is 'autoload' then
@@ -588,4 +588,3 @@ interface SearchableModule {
*/
public function search($text, array $options = array());
}

View File

@@ -236,6 +236,9 @@ class Modules extends WireArray {
*/
public function __construct($path) {
parent::__construct();
$this->nameProperty = 'className';
$this->usesNumericKeys = false;
$this->indexedByName = true;
$this->addPath($path); // paths[0] is always core modules path
}
@@ -733,8 +736,8 @@ class Modules extends WireArray {
* #pw-internal
*
* @param string|object $moduleName Module instance or module name
* @param User $user Optionally specify different user to consider than current.
* @param Page $page Optionally specify different page to consider than current.
* @param User|null $user Optionally specify different user to consider than current.
* @param Page|null $page Optionally specify different page to consider than current.
* @param bool $strict If module specifies no permission settings, assume no permission.
* - Default (false) is to assume permission when module doesn't say anything about it.
* - Process modules (for instance) generally assume no permission when it isn't specifically defined
@@ -743,7 +746,7 @@ class Modules extends WireArray {
* @return bool
*
*/
public function hasPermission($moduleName, User $user = null, Page $page = null, $strict = false) {
public function hasPermission($moduleName, ?User $user = null, ?Page $page = null, $strict = false) {
return $this->loader->hasPermission($moduleName, $user, $page, $strict);
}
@@ -978,7 +981,7 @@ class Modules extends WireArray {
$currentVersion = $class === 'PHP' ? PHP_VERSION : $this->wire()->config->version;
}
} else {
$installed = parent::get($class) !== null;
$installed = isset($this->data[$class]);
if($installed && $requiredVersion !== null) {
$info = $this->info->getModuleInfo($class);
$currentVersion = $info['version'];
@@ -1233,12 +1236,14 @@ class Modules extends WireArray {
*
* #pw-internal
*
* @param string|int|Module $name
* @param string|Module $name
* @param int|null|false $setID Optionally set module ID or false to unset
* @return int
*
*/
public function moduleID($name, $setID = null) {
if($name instanceof Module) $name = $name->className();
if(strpos("$name", '\\') !== false) $name = wireClassName($name, false);
if($setID !== null) {
if($setID === false) {
unset($this->moduleIDs[$name]);
@@ -1269,7 +1274,8 @@ class Modules extends WireArray {
if($setName === null) return $name;
$id = $this->getModuleID($name);
} else if(!ctype_digit("$id")) {
if(is_string($id)) return $id;
if(strpos("$id", '\\') !== false) $id = wireClassName($id, false);
if($setName === null && is_string($id)) return $id;
$id = $this->getModuleID($id);
}
$id = (int) $id;
@@ -1291,23 +1297,17 @@ class Modules extends WireArray {
$id = 0;
if(ctype_digit("$class")) {
return (int) $class;
} else if(isset($this->moduleIDs["$class"])) {
return (int) $this->moduleIDs["$class"];
}
if(ctype_digit("$class")) return (int) $class;
if(isset($this->moduleIDs["$class"])) return (int) $this->moduleIDs["$class"];
if(is_object($class)) {
if($class instanceof Module) {
$class = $this->getModuleClass($class);
if(isset($this->moduleIDs[$class])) {
return (int) $this->moduleIDs[$class];
}
} else {
// Class is not a module
return $id;
}
if(!$class instanceof Module) return 0; // class is not a module
$class = $this->getModuleClass($class);
} else if(strpos("$class", '\\') !== false) {
$class = wireClassName($class, false);
}
if(isset($this->moduleIDs["$class"])) return (int) $this->moduleIDs["$class"];
foreach($this->info->moduleInfoCache as $key => $info) {
if(is_string($info)) {
@@ -1369,26 +1369,26 @@ class Modules extends WireArray {
if(strpos($module, '.') !== false) {
$module = basename(basename($module, '.php'), '.module');
}
if(array_key_exists($module, $this->moduleIDs)) {
if(isset($this->data[$module])) {
$className = $module;
} else if(array_key_exists($module, $this->moduleIDs)) {
$className = $module;
} else if(array_key_exists($module, $this->installableFiles)) {
$className = $module;
}
}
if($className) {
if($withNamespace) {
if($namespace) {
$className = "$namespace\\$className";
} else {
$className = $this->info->getModuleNamespace($className) . $className;
}
if(!$className) return false;
if($withNamespace) {
if($namespace) {
$className = "$namespace\\$className";
} else {
$className = $this->info->getModuleNamespace($className) . $className;
}
return $className;
}
return false;
}
return $className;
}
/**
@@ -1740,7 +1740,7 @@ class Modules extends WireArray {
* @return InputfieldWrapper|null
*
*/
public function ___getModuleConfigInputfields($moduleName, InputfieldWrapper $form = null) {
public function ___getModuleConfigInputfields($moduleName, ?InputfieldWrapper $form = null) {
return $this->configs->getModuleConfigInputfields($moduleName, $form);
}

View File

@@ -558,7 +558,7 @@ class ModulesConfigs extends ModulesClass {
* @return InputfieldWrapper|null
*
*/
public function getModuleConfigInputfields($moduleName, InputfieldWrapper $form = null) {
public function getModuleConfigInputfields($moduleName, ?InputfieldWrapper $form = null) {
$moduleName = $this->modules->getModuleClass($moduleName);
$configurable = $this->isConfigurable($moduleName);

View File

@@ -309,6 +309,10 @@ class ModulesFiles extends ModulesClass {
if(class_exists(__NAMESPACE__ . "\\$moduleName", false)) return true;
// next do a slower check, figuring out namespace
$ns = $this->modules->info->getModuleNamespace($moduleName, array('file' => $file));
if($ns === null) {
// unable to determine module namespace, likely file does not exist
$ns = (string) $ns;
}
$className = trim($ns, "\\") . "\\$moduleName";
if(class_exists($className, false)) return true;
// if this point is reached, module is not yet in memory in either instance
@@ -430,6 +434,7 @@ class ModulesFiles extends ModulesClass {
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
$class = $row['class'];
if(strpos($class, '.') === 0) continue;
$file = $this->getModuleFile($class, array('fast' => true));

View File

@@ -1364,7 +1364,7 @@ class ModulesInfo extends ModulesClass {
*
*/
public function getNamespacePath($namespace) {
if($namespace === 'ProcessWire') return "ProcessWire\\";
if($namespace === 'ProcessWire') return false; // not unique module namespace
if(is_null($this->moduleNamespaceCache)) $this->getNamespaces();
$namespace = "\\" . trim($namespace, "\\") . "\\";
return isset($this->moduleNamespaceCache[$namespace]) ? $this->moduleNamespaceCache[$namespace] : false;

View File

@@ -203,9 +203,11 @@ class ModulesInstaller extends ModulesClass {
$permissions->save($permission);
if($languages) $languages->unsetDefault();
$this->message(sprintf($this->_('Added Permission: %s'), $permission->name));
if($languages) $languages->setDefault();
} catch(\Exception $e) {
if($languages) $languages->unsetDefault();
$error = sprintf($this->_('Error adding permission: %s'), $name);
if($languages) $languages->setDefault();
$this->trackException($e, false, $error);
}
}

View File

@@ -412,7 +412,7 @@ class ModulesLoader extends ModulesClass {
$requires = array();
$name = $moduleName;
$moduleName = $this->loadModule($path, $pathname, $requires, $installed);
if(!$config->paths->get($name)) $modulesFiles->setConfigPaths($name, dirname($basePath . $pathname));
if(!$config->paths->__isset($name)) $modulesFiles->setConfigPaths($name, dirname($basePath . $pathname));
if(!$moduleName) continue;
if(count($requires)) {
@@ -737,8 +737,8 @@ class ModulesLoader extends ModulesClass {
* #pw-internal
*
* @param string|object $moduleName Module instance or module name
* @param User $user Optionally specify different user to consider than current.
* @param Page $page Optionally specify different page to consider than current.
* @param User|null $user Optionally specify different user to consider than current.
* @param Page|null $page Optionally specify different page to consider than current.
* @param bool $strict If module specifies no permission settings, assume no permission.
* - Default (false) is to assume permission when module doesn't say anything about it.
* - Process modules (for instance) generally assume no permission when it isn't specifically defined
@@ -747,7 +747,7 @@ class ModulesLoader extends ModulesClass {
* @return bool
*
*/
public function hasPermission($moduleName, User $user = null, Page $page = null, $strict = false) {
public function hasPermission($moduleName, ?User $user = null, ?Page $page = null, $strict = false) {
if(is_object($moduleName)) {
$module = $moduleName;

View File

@@ -507,6 +507,12 @@ class NoticeWarning extends Notice {
class Notices extends WireArray {
const logAllNotices = false; // for debugging/dev purposes
public function __construct() {
parent::__construct();
$this->usesNumericKeys = true;
$this->indexedByName = false;
}
/**
* Initialize Notices API var
@@ -548,8 +554,9 @@ class Notices extends WireArray {
*
*/
protected function allowNotice(Notice $item) {
$user = $this->wire()->user;
// intentionally not using $this->wire()->user; in case this gets called early in boot
$user = $this->wire('user');
if($item->flags & Notice::debug) {
if(!$this->wire()->config->debug) return false;

View File

@@ -65,6 +65,7 @@ class NullPage extends Page implements WireNull {
*
* @param string $selector
* @return null
* @todo can this return NullPage instead?
*
*/
public function parent($selector = '') { return null; }
@@ -97,6 +98,14 @@ class NullPage extends Page implements WireNull {
*/
public function isHidden() { return true; }
/**
* #pw-internal
*
* @return bool
*
*/
public function isNew() { return false; }
/**
* #pw-internal
*

View File

@@ -8,7 +8,7 @@
* 1. Providing get/set access to the Page's properties
* 2. Accessing the related hierarchy of pages (i.e. parents, children, sibling pages)
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* #pw-summary Class used by all Page objects in ProcessWire.
@@ -68,8 +68,9 @@
* @property string $filesPath Get the disk path to store files for this page, creating it if it does not exist. #pw-group-files
* @property string $filesUrl Get the URL to store files for this page, creating it if it does not exist. #pw-group-files
* @property bool $hasFiles Does this page have one or more files in its files path? #pw-group-files
* @property bool $outputFormatting Whether output formatting is enabled or not. #pw-advanced
* @property bool $outputFormatting Whether output formatting is enabled or not. Same as calling $page->of() with no arguments. #pw-advanced
* @property int $sort Sort order of this page relative to siblings (applicable when manual sorting is used). #pw-group-system
* @property int|null $sortPrevious Previous sort order, if changed (3.0.235+) #pw-group-system
* @property int $index Index of this page relative to its siblings, regardless of sort (starting from 0). #pw-group-traversal
* @property string $sortfield Field that a page is sorted by relative to its siblings (default="sort", which means drag/drop manual) #pw-group-system
* @property null|array _statusCorruptedFields Field names that caused the page to have Page::statusCorrupted status. #pw-internal
@@ -104,7 +105,7 @@
*
* Methods added by PageRender.module:
* -----------------------------------
* @method string|mixed render($fieldName = '') Returns rendered page markup. If given a $fieldName argument, it behaves same as the renderField() method. #pw-group-output-rendering
* @method string|mixed render($arg1 = null, $arg2 = null) Returns rendered page markup. Please see the `PageRender::renderPage()` method for arguments and usage details. #pw-group-output-rendering
*
* Methods added by PagePermissions.module:
* ----------------------------------------
@@ -119,6 +120,7 @@
* @method bool addable($pageToAdd = null) Returns true if the current user can add children to the page, false if not. Optionally specify the page to be added for additional access checking. #pw-group-access
* @method bool moveable($newParent = null) Returns true if the current user can move this page. Optionally specify the new parent to check if the page is moveable to that parent. #pw-group-access
* @method bool sortable() Returns true if the current user can change the sort order of the current page (within the same parent). #pw-group-access
* @method bool cloneable($recursive = null) Can current user clone this page? Specify false for $recursive argument to ignore whether children are cloneable. @since 3.0.239 #pw-group-access
* @property bool $viewable #pw-group-access
* @property bool $editable #pw-group-access
* @property bool $publishable #pw-group-access
@@ -129,6 +131,7 @@
* @property bool $moveable #pw-group-access
* @property bool $sortable #pw-group-access
* @property bool $listable #pw-group-access
* @property bool $cloneable @since 3.0.239 #pw-group-access
*
* Methods added by PagePathHistory.module (installed by default)
* --------------------------------------------------------------
@@ -139,7 +142,9 @@
* Methods added by LanguageSupport.module (not installed by default)
* -----------------------------------------------------------------
* @method Page setLanguageValue($language, $field, $value) Set value for field in language (requires LanguageSupport module). $language may be ID, language name or Language object. Field should be field name (string). #pw-group-languages
* @method Page getLanguageValue($language, $field) Get value for field in language (requires LanguageSupport module). $language may be ID, language name or Language object. Field should be field name (string). #pw-group-languages
* @method Page setLanguageValues($field, array $values) Set value for field in one or more languages (requires LanguageSupport module). $field should be field/property name (string), $values should be array of values indexed by language name. @since 3.0.236 #pw-group-languages
* @method mixed getLanguageValue($language, $field) Get value for field in language (requires LanguageSupport module). $language may be ID, language name or Language object. Field should be field name (string). #pw-group-languages
* @method array getLanguageValues($field, array $langs = []) Get values for field or one or more languages (requires LanguageSupport module). $field should be field/property name (string), $langs should be array of language names, or omit for all languages. Returns array of values indexed by language name. @since 3.0.236 #pw-group-languages
*
* Methods added by LanguageSupportPageNames.module (not installed by default)
* ---------------------------------------------------------------------------
@@ -147,6 +152,10 @@
* @method string localPath($language = null) Return the page path in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages #pw-group-urls
* @method string localUrl($language = null) Return the page URL in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages #pw-group-urls
* @method string localHttpUrl($language = null) Return the page URL (including scheme and hostname) in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages #pw-group-urls
* @method Page setLanguageStatus($language, $status = null) Set active status for language(s), can be called as `$page->setLanguageStatus('es', true);` or `$page->setLanguageStatus([ 'es' => true, 'br' => false ]);` to set multiple. @since 3.0.236 #pw-group-languages
* @method array|bool getLanguageStatus($language = []) Get active status for language(s). If given a $language (Language or name of language) it returns a boolean. If given multiple language names (array), or argument omitted, it returns array like `[ 'default' => true, 'fr' => false ];`. @since 3.0.236 #pw-group-languages
* @method Page setLanguageName($language, $name = null) Set page name for language with `$page->setLanguageName('es', 'hola');` or set multiple with `$page->setLanguageName([ 'default' => 'hello', 'es' => 'hola' ]);` @since 3.0.236 #pw-group-languages
* @method array|string getLanguageName($language = []) Get page name for language(s). If given a Language object, it returns a string. If given array of language names, or argument omitted, it returns an array like `[ 'default' => 'hello', 'es' => 'hola' ];`. @since 3.0.236 #pw-group-languages
*
* Methods added by PageFrontEdit.module (not always installed by default)
* -----------------------------------------------------------------------
@@ -154,7 +163,7 @@
*
* Methods added by ProDrafts.module (if installed)
* ------------------------------------------------
* @method ProDraft|\ProDraft|int|string|Page|array draft($key = null, $value = null) Helper method for drafts (added by ProDrafts). #pw-advanced
* @method ProDraft|int|string|Page|array draft($key = null, $value = null) Helper method for drafts (added by ProDrafts). #pw-advanced
*
* Hookable methods
* ----------------
@@ -377,7 +386,15 @@ class Page extends WireData implements \Countable, WireMatchable {
* @var string
*
*/
private $namePrevious;
private $namePrevious;
/**
* The previous sort value used by page, if changed during runtime.
*
* @var int
*
*/
private $sortPrevious;
/**
* The previous status used by this page, if it changed during runtime.
@@ -588,10 +605,10 @@ class Page extends WireData implements \Countable, WireMatchable {
/**
* Create a new page in memory.
*
* @param Template $tpl Template object this page should use.
* @param Template|null $tpl Template object this page should use.
*
*/
public function __construct(Template $tpl = null) {
public function __construct(?Template $tpl = null) {
parent::__construct();
if($tpl !== null) {
$tpl->wire($this);
@@ -601,6 +618,7 @@ class Page extends WireData implements \Countable, WireMatchable {
$this->parentPrevious = null;
$this->templatePrevious = null;
$this->statusPrevious = null;
$this->sortPrevious = null;
}
/**
@@ -700,7 +718,8 @@ class Page extends WireData implements \Countable, WireMatchable {
$this->setStatus($value);
break;
case 'statusPrevious':
$this->statusPrevious = is_null($value) ? null : (int) $value;
case 'sortPrevious':
$this->$key = is_null($value) ? null : (int) $value;
break;
case 'name':
$this->setName($value);
@@ -1150,6 +1169,58 @@ class Page extends WireData implements \Countable, WireMatchable {
return $this->values()->getDotValue($this, $key);
}
/**
* Preload multiple fields together as a group (experimental)
*
* This is an optimization that enables you to load the values for multiple fields into
* a page at once, and often in a single query. For fields where it is supported, and
* for cases where you have a lot of fields to load at once, it can be up to 50% faster
* than the default of lazy-loading fields.
*
* To use, call `$page->preload([ 'field1', 'field2', 'etc.' ])` before accessing
* `$page->field1`, `$page->field2`, etc.
*
* The more fields you give this method, the more performance improvement it can offer.
* As a result, don't bother if with only a few fields, as it's less likely to make
* a difference at small scale. You will also see a more measurable benefit if preloading
* fields for lots of pages at once.
*
* Preload works with some Fieldtypes and not others. For details on what it is doing,
* specify `true` for the `debug` option which will make it return array of what it
* loaded and what it didn't. Have a look at this array with TracyDebugger or output
* a print_r() call on it, and the result is self explanatory.
*
* NOTE: This function is currently experimental, recommended for testing only.
*
* ~~~~~
* // Example usage
* $page->preload([ 'headline', 'body', 'sidebar', 'intro', 'summary' ]);
* echo "
* <h1 id='headline'>$page->headline</h1>";
* <div id='intro'>$page->intro</div>
* <div id='body'>$page->body</div>
* <aside id='sidebar' pw-append>$page->sidebar</aside>
* <meta id='meta-description'>$page->summary</meta>
* ";
* ~~~~~
*
* @param array $fieldNames Names of fields to preload or omit (or blank array)
* to preload all supported fields.
* @param array $options Options to modify default behavior:
* - `debug` (bool): Specify true to return additional info in returned array (default=false).
* - See the `PagesLoader::preloadFields()` method for additional options.
* @return array Array of details
* @since 3.0.243
*
*/
public function preload(array $fieldNames = array(), $options = array()) {
if(empty($fieldNames)) {
return $this->wire()->pages->loader()->preloadAllFields($this, $options);
} else {
return $this->wire()->pages->loader()->preloadFields($this, $fieldNames, $options);
}
}
/**
* Hookable method called when a request to a field was made that didn't match anything
*
@@ -1819,6 +1890,8 @@ class Page extends WireData implements \Countable, WireMatchable {
* - When a string or array, a selector is assumed and quantity will be counted based on selector.
* - When boolean true, number includes only visible children (excludes unpublished, hidden, no-access, etc.)
* - When boolean false, number includes all children without conditions, including unpublished, hidden, no-access, etc.
* - When integer 1 number includes “viewable” children (as opposed to “visible” children, viewable children includes
* hidden pages and also includes unpublished pages if user has page-edit permission).
* @return int Number of children
* @see Page::hasChildren(), Page::children(), Page::child()
*
@@ -2122,7 +2195,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* @return Page|NullPage Returns the next sibling page, or a NullPage if none found.
*
*/
public function next($selector = '', PageArray $siblings = null) {
public function next($selector = '', ?PageArray $siblings = null) {
if($selector instanceof PageArray) {
$siblings = $selector;
$selector = '';
@@ -2179,7 +2252,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* @return PageArray
*
*/
public function nextUntil($selector = '', $filter = '', PageArray $siblings = null) {
public function nextUntil($selector = '', $filter = '', ?PageArray $siblings = null) {
if($siblings === null && $this->traversalPages) $siblings = $this->traversalPages;
if($siblings) return $this->traversal()->nextUntilSiblings($this, $selector, $filter, $siblings);
return $this->traversal()->nextUntil($this, $selector, $filter);
@@ -2203,7 +2276,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* @return Page|NullPage Returns the previous sibling page, or a NullPage if none found.
*
*/
public function prev($selector = '', PageArray $siblings = null) {
public function prev($selector = '', ?PageArray $siblings = null) {
if($selector instanceof PageArray) {
$siblings = $selector;
$selector = '';
@@ -2242,7 +2315,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* @return PageArray
*
*/
public function prevUntil($selector = '', $filter = '', PageArray $siblings = null) {
public function prevUntil($selector = '', $filter = '', ?PageArray $siblings = null) {
if($siblings === null && $this->traversalPages) $siblings = $this->traversalPages;
if($siblings) return $this->traversal()->prevUntilSiblings($this, $selector, $filter, $siblings);
return $this->traversal()->prevUntil($this, $selector, $filter);
@@ -2329,7 +2402,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* @param array $options See Pages::save() documentation for options. You may also specify $options as the first argument if no $field is needed.
* @return bool Returns true on success false on fail
* @throws WireException on database error
* @see Pages::save(), Pages::saveField(), Pages::saveReady(), Pages::saveFieldReady(), Pages::saved(), Pages::fieldSaved()
* @see Pages::save(), Page::saveFields(), Pages::saveField(), Pages::saveReady(), Pages::saveFieldReady(), Pages::saved(), Pages::fieldSaved()
*
*/
public function save($field = null, array $options = array()) {
@@ -2355,6 +2428,21 @@ class Page extends WireData implements \Countable, WireMatchable {
return $pages->save($this, $options);
}
/**
* Save only the given named fields for this page
*
* @param array|string $fields Array of field name(s) or string (CSV or space separated)
* @param array $options See Pages::save() documentation for options.
* @return array Names of fields that were saved
* @throws WireException on database error
* @see Page::save()
* @since 3.0.242
*
*/
public function saveFields($fields, array $options = array()) {
return $this->wire()->pages->saveFields($this, $fields, $options);
}
/**
* Quickly set field value(s) and save to database
@@ -2406,6 +2494,7 @@ class Page extends WireData implements \Countable, WireMatchable {
if($of) $this->of(false);
foreach($values as $k => $v) {
$this->set($k, $v);
if(!$property) $this->trackChange($k);
}
if($property) {
$result = $this->save($property, $options);
@@ -2843,6 +2932,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* - `http` (bool): True to force scheme and hostname in URL (default=auto detect).
* - `language` (Language|bool): Optionally specify Language to start editor in, or boolean true to force current user language.
* - `find` (string): Name of field to find in the editor (3.0.151+)
* - `vars` (array): Additional variables to include in query string (3.0.239+)
* @return string URL for editing this page
*
*/
@@ -2929,16 +3019,18 @@ class Page extends WireData implements \Countable, WireMatchable {
* - `$page->render->fieldName;`
* - `$page->_fieldName_;`
*
* This method expects that there is a file in `/site/templates/fields/` to render the field with:
* This method expects that there is a file in `/site/templates/fields/` to render the field with
* one of the following:
*
* - `/site/templates/fields/fieldName.php`
* - `/site/templates/fields/fieldName.templateName.php`
* - `/site/templates/fields/fieldName/$file.php` (using $file argument)
* - `/site/templates/fields/$file.php` (using $file argument)
* - `/site/templates/fields/$file/fieldName.php` (using $file argument, must have trailing slash)
* - `/site/templates/fields/$file.fieldName.php` (using $file argument, must have trailing period)
* - `/site/templates/fields/fieldName/$file.php`
* - `/site/templates/fields/$file.php`
* - `/site/templates/fields/$file/fieldName.php`
* - `/site/templates/fields/$file.fieldName.php`
*
* Note that the examples above showing $file require that the `$file` argument is specified.
* Note that the examples above showing $file require that the `$file` argument is specified
* in the `renderField()` method call.
*
* ~~~~~
* // Render output for the 'images' field (assumes you have implemented an output file)
@@ -2948,8 +3040,8 @@ class Page extends WireData implements \Countable, WireMatchable {
* #pw-group-output-rendering
*
* @param string $fieldName May be any custom field name or native page property.
* @param string $file Optionally specify file (in site/templates/fields/) to render with (may omit .php extension).
* @param mixed|null $value Optionally specify value to render, otherwise it will be pulled from this $page.
* @param string $file Optionally specify file (in site/templates/fields/) to render with (may optionally omit .php extension).
* @param mixed|null $value Optionally specify value to render, otherwise it will be pulled from this page.
* @return mixed|string Returns the rendered value of the field
* @see Page::render(), Page::renderValue()
*
@@ -3000,6 +3092,23 @@ class Page extends WireData implements \Countable, WireMatchable {
*
*/
public function getInputfields($fieldName = '') {
if($this->wire()->hooks->isMethodHooked($this, 'getInputfields')) {
return $this->__call('getInputfields', array($fieldName));
} else {
return $this->___getInputfields($fieldName);
}
}
/**
* Hookable version of getInputfields() method.
*
* See the getInputfields() method above for documentation details.
*
* @param string|array $fieldName
* @return null|InputfieldWrapper Returns an InputfieldWrapper array of Inputfield objects, or NULL on failure.
*
*/
protected function ___getInputfields($fieldName = '') {
return $this->values()->getInputfields($this, $fieldName);
}
@@ -3472,7 +3581,7 @@ class Page extends WireData implements \Countable, WireMatchable {
*
* The output formatting state determines if a page's output is allowed to be filtered by runtime formatters.
* Pages used for output should have output formatting on. Pages you intend to manipulate and save should
* have it off.
* have it off. See this post about [output formatting](https://processwire.com/blog/posts/output-formatting/).
*
* ~~~~~
* // Set output formatting state off, for page manipulation
@@ -3489,6 +3598,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* @param bool $outputFormatting Optional, default true
* @return $this
* @see Page::outputFormatting(), Page::of()
* @link https://processwire.com/blog/posts/output-formatting/
*
*/
public function setOutputFormatting($outputFormatting = true) {
@@ -3519,6 +3629,8 @@ class Page extends WireData implements \Countable, WireMatchable {
* - Pages used for front-end output should have output formatting turned ON.
*
* - Pages that you are manipulating and saving should have output formatting turned OFF.
*
* See this post about [output formatting](https://processwire.com/blog/posts/output-formatting/).
*
* ~~~~~
* // Set output formatting state off, for page manipulation
@@ -3533,6 +3645,7 @@ class Page extends WireData implements \Countable, WireMatchable {
*
* @param bool $outputFormatting If specified, sets output formatting state ON or OFF. If not specified, nothing is changed.
* @return bool Current output formatting state (before this function call, if it was changed)
* @link https://processwire.com/blog/posts/output-formatting/
*
*/
public function of($outputFormatting = null) {
@@ -3985,7 +4098,7 @@ class Page extends WireData implements \Countable, WireMatchable {
*
* ~~~~~
* // set and save a meta value
* $page->meta()->set('colors', [ 'red, 'green', 'blue' ]);
* $page->meta()->set('colors', [ 'red', 'green', 'blue' ]);
*
* // get a meta value
* $colors = $page->meta()->get('colors');
@@ -4040,6 +4153,8 @@ class Page extends WireData implements \Countable, WireMatchable {
$this->namePrevious = $old;
} else if($what === 'status' && $old !== null) {
$this->statusPrevious = (int) $old;
} else if($what === 'sort' && $old !== null && $this->sortPrevious === null) {
$this->sortPrevious = (int) $old;
}
}
return parent::trackChange($what, $old, $new);

View File

@@ -28,7 +28,7 @@
* ~~~~~
* #pw-body
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* @method string getMarkup($key = null) Render a simple/default markup value for each item #pw-internal
@@ -74,6 +74,16 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*/
protected $keyIndex = array();
/**
* Construct
*
*/
public function __construct() {
parent::__construct();
$this->indexedByName = false;
$this->usesNumericKeys = true;
}
/**
* Template method that descendant classes may use to validate items added to this WireArray
*
@@ -142,18 +152,6 @@ class PageArray extends PaginatedArray implements WirePaginatable {
}
}
/**
* Does this PageArray use numeric keys only? (yes it does)
*
* Defined here to override the slower check in WireArray
*
* @return bool
*
*/
protected function usesNumericKeys() {
return true;
}
/**
* Per WireArray interface, return a blank Page
*
@@ -394,7 +392,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
* This is applicable to and destructive to the WireArray.
*
* @param string|Selectors|array $selectors AttributeSelector string to use as the filter.
* @param string|Selectors|array $selectors Selector string to use as the filter.
* @param bool|int $not Make this a "not" filter? Use int 1 for "not all". (default is false)
* @return PageArray|WireArray reference to current [filtered] PageArray
*
@@ -409,7 +407,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
* #pw-internal
*
* @param string $selector AttributeSelector string to use as the filter.
* @param string $selector Selector string to use as the filter.
* @return PageArray|PaginatedArray|WireArray reference to current PageArray instance.
*
*/
@@ -418,11 +416,11 @@ class PageArray extends PaginatedArray implements WirePaginatable {
}
/**
* Filter out pages that don't match the selector (destructive)
* Filter out pages that DO match the selector (destructive)
*
* #pw-internal
*
* @param string $selector AttributeSelector string to use as the filter.
* @param string $selector Selector string to use
* @return PageArray|PaginatedArray|WireArray reference to current PageArray instance.
*
*/
@@ -454,7 +452,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
* #pw-internal
*
* @param string $selector AttributeSelector string.
* @param string $selector Selector string.
* @return PageArray|WireArray New PageArray instance
* @see WireArray::find()
*
@@ -621,10 +619,16 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
*/
public function __toString() {
$s = '';
foreach($this as $page) $s .= "$page|";
$s = rtrim($s, "|");
return $s;
$ids = array();
if($this->lazyLoad) {
$items = $this;
} else {
$items = &$this->data;
}
foreach($items as $page) {
if(!$page instanceof NullPage) $ids[] = $page->id;
}
return implode('|', $ids);
}
/**
@@ -649,7 +653,8 @@ class PageArray extends PaginatedArray implements WirePaginatable {
if($out) {
$out = "<ul>$out</ul>";
if($this->getLimit() && $this->getTotal() > $this->getLimit()) {
$pager = $this->wire('modules')->get('MarkupPagerNav');
/** @var MarkupPagerNav $pager */
$pager = $this->wire()->modules->get('MarkupPagerNav');
$out .= $pager->render($this);
}
}
@@ -729,5 +734,3 @@ class PageArray extends PaginatedArray implements WirePaginatable {
}
}
}

View File

@@ -177,8 +177,8 @@ class PageComparison {
// action is just a string to return
$result = $action;
}
} else if(is_callable($action)) {
} else if(is_callable($action) && (!is_object($action) || $action instanceof \Closure)) {
// action is callable
$result = call_user_func_array($action, array($val, $key, $page));
@@ -214,7 +214,7 @@ class PageComparison {
// exit early for simple path comparison
return true;
} else if($page->name === $s) {
// early exit for simple name atch
// early exit for simple name match
return true;
} else if(Selectors::stringHasOperator($s)) {
// selectors string
@@ -243,39 +243,140 @@ class PageComparison {
if(!empty($options['useDatabase'])) {
$selectors->add(new SelectorEqual('id', $page->id))->add(new SelectorEqual('include', 'all'));
return $page->wire()->pages->count($selectors) > 0;
}
}
$matches = false;
$matchFail = false;
$groupSelectors = array(); // same (1) item match group selectors
$orGroupSelectors = array(); // OR-group selectors
foreach($selectors as $selector) {
$property = $selector->field;
$subproperty = '';
if(is_array($property)) $property = reset($property);
if(strpos($property, '.')) list($property, $subproperty) = explode('.', $property, 2);
if(in_array($property, $this->matchesIgnores)) continue;
$matches = true;
$value = $page->getUnformatted($property);
if(is_object($value)) {
// convert object to array value(s)
$value = $this->getObjectValueArray($value, $subproperty);
} else if(is_array($value)) {
// ok: selector matches will accept an array
} else {
// convert to a string value, whatever it may be
$value = "$value";
if($selector->quote === '(') {
// OR-groups are handled below this loop
$orGroup = $selector->field() . '.';
if(!isset($orGroupSelectors[$orGroup])) $orGroupSelectors[$orGroup] = array();
$orGroupSelectors[$orGroup][] = $selector;
continue;
}
if(!$selector->matches($value)) {
$matches = false;
if(!$this->selectorMatches($page, $selector)) {
$matchFail = true;
break;
}
if($selector->group !== null) {
// validate that same (1) item matches from these later
$groupName = $selector->group;
if(!isset($groupSelectors[$groupName])) $groupSelectors[$groupName] = array();
$groupSelectors[$groupName][] = $selector;
}
}
// OR-groups
if(!$matchFail && count($orGroupSelectors)) {
foreach($orGroupSelectors as /* $orGroupName => */ $selectors) {
$orGroupMatches = false;
foreach($selectors as $selector) {
if($this->matches($page, $selector->value)) {
// OR-group selector matches
$orGroupMatches = true;
break;
}
}
if(!$orGroupMatches) {
$matchFail = true;
break;
}
}
}
// same (1) item match groups
if(!$matchFail && count($groupSelectors)) {
foreach($groupSelectors as /* $groupName => */ $selectors) {
$matchGroupKeys = null;
foreach($selectors as $selector) {
$keys = $selector->get('_matchGroupKeys'); // populated by selectorMatchesProperty
if($matchGroupKeys === null) {
$matchGroupKeys = $keys;
} else {
$matchGroupKeys = array_intersect($matchGroupKeys, $keys);
}
}
if(empty($matchGroupKeys)) {
$matchFail = true;
break;
}
}
}
return !$matchFail;
}
return $matches;
/**
* Return whether individual Selector object matches Page
*
* @param Page $page
* @param Selector $selector
* @return bool
* @since 3.0.231
*
*/
protected function selectorMatches(Page $page, Selector $selector) {
$match = false;
$properties = $selector->fields();
foreach($properties as $property) {
$match = $this->selectorMatchesProperty($page, $selector, $property);
if($match) break;
}
return $match;
}
/**
* Return whether single property from individual Selector matches Page
*
* @param Page $page
* @param Selector $selector
* @param string $property
* @return bool
* @since 3.0.231
*
*/
protected function selectorMatchesProperty(Page $page, Selector $selector, $property) {
$subproperty = '';
if(strpos($property, '.')) list($property, $subproperty) = explode('.', $property, 2);
if(in_array($property, $this->matchesIgnores)) return true;
if($selector->quote === '[' && Selectors::stringHasOperator($selector->value())) {
$selector->value = $page->wire()->pages->findIDs($selector->value());
}
$value = $page->getUnformatted($property);
if(is_object($value)) {
// convert object to array value(s)
$value = $this->getObjectValueArray($value, $subproperty);
} else if(is_array($value)) {
// ok: selector matches will accept an array
} else {
// convert to a string value, whatever it may be
$value = "$value";
}
if(!$selector->matches($value)) return false;
if($selector->group !== null && is_array($value)) {
// see which individual values match and record their keys for later comparison
$matchGroupKeys = array();
foreach($value as $key => $val) {
if($selector->matches($val)) $matchGroupKeys[] = $key;
}
$selector->setQuietly('_matchGroupKeys', $matchGroupKeys); // used by matches() method
}
return true;
}
/**

View File

@@ -5,7 +5,7 @@
*
* Matches selector strings to pages
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
* https://processwire.com
*
* Hookable methods:
@@ -1817,6 +1817,7 @@ class PageFinder extends Wire {
$tableAlias = $database->escapeTable($tableAlias);
$join = '';
$joinType = '';
$numEmptyValues = 0;
$valueArray = $selector->values(true);
$fieldtype = $field->type;
@@ -1835,7 +1836,12 @@ class PageFinder extends Wire {
if(in_array($operator, array('=', '!=', '<', '<=', '>', '>='))) {
// we only accommodate this optimization for single-value selectors...
if($this->whereEmptyValuePossible($field, $subfield, $selector, $query, $value, $whereFields)) {
if(count($valueArray) > 1 && $operator == '=') $whereFieldsType = 'OR';
if(count($valueArray) > 1) {
if($operator == '=') $whereFieldsType = 'OR';
} else {
$fieldCnt[$field->table]--;
if($fieldCnt[$field->table] < 1) unset($fieldCnt[$field->table]);
}
continue;
}
}
@@ -1846,6 +1852,7 @@ class PageFinder extends Wire {
$q = $subqueries[$tableAlias];
} else {
$q = $this->wire(new DatabaseQuerySelect());
// $subqueries[$tableAlias] = $q;
}
/** @var PageFinderDatabaseQuerySelect $q */
@@ -1855,12 +1862,25 @@ class PageFinder extends Wire {
$q->set('selectors', $selectors); // original selectors (all) if required by the fieldtype
$q->set('parentQuery', $query);
$q->set('pageFinder', $this);
$q->set('joinType', $joinType);
$q->bindOption('global', true); // ensures bound value key are globally unique
$q->bindOption('prefix', 'pf'); // pf=PageFinder
/* @todo To be implemented after 3.0.245
if(strpos($subfields, 'JSON.') === 0) {
if($this->getMatchQueryJSON($q, $tableAlias, $subfields, $selector->operator, $value)) {
continue;
}
}
*/
$q = $fieldtype->getMatchQuery($q, $tableAlias, $subfield, $selector->operator, $value);
$q->copyTo($query, array('select', 'join', 'leftjoin', 'orderby', 'groupby'));
$q->copyBindValuesTo($query);
if($q->joinType && $q->joinType != $joinType) {
$joinType = strtolower((string) $q->joinType);
}
if(count($q->where)) {
// $and = $selector->not ? "AND NOT" : "AND";
@@ -1883,9 +1903,8 @@ class PageFinder extends Wire {
}
if($join) {
$joinType = 'join';
if(count($fields) > 1
if($joinType === 'leftjoin'
|| count($fields) > 1
|| !empty($options['startAfterID']) || !empty($options['stopBeforeID'])
|| (count($valueArray) > 1 && $numEmptyValues > 0)
|| ($subfield == 'count' && !$this->isRepeaterFieldtype($field->type))
@@ -1893,7 +1912,7 @@ class PageFinder extends Wire {
|| $selector->operator == '!=') {
// join should instead be a leftjoin
$joinType = "leftjoin";
$joinType = 'leftjoin';
if($where) {
$whereType = $lastSelector->str == $selector->str ? "OR" : ") AND (";
@@ -1905,6 +1924,8 @@ class PageFinder extends Wire {
// removes condition from join, but ensures we still have a $join
$join = '1=1';
}
} else {
$joinType = 'join';
}
// we compile the joins after going through all the selectors, so that we can
@@ -1973,6 +1994,22 @@ class PageFinder extends Wire {
return $query;
}
/**
* Get match query when data is stored in a JSON DB column (future use)
*
* @param PageFinderDatabaseQuerySelect DatabaseQuerySelect $q
* @param string $tableAlias
* @param string $subfields
* @param string $operator
* @param string|int|array $value
* @return bool
*
*/
protected function getMatchQueryJSON(DatabaseQuerySelect $q, $tableAlias, $subfields, $operator, $value) {
// @todo to be implemented after 3.0.245
return false;
}
/**
* Post process a DatabaseQuerySelect for page finder
*
@@ -2138,7 +2175,9 @@ class PageFinder extends Wire {
} else if($operator === '!=' || $operator === '<>') {
// not equals
$whereType = 'AND';
$whereType = count($selector->fields()) > 1 && $ft->isEmptyValue($field, $value) ? 'OR' : 'AND';
// alternate and technically more consistent behavior, but doesn't seem useful:
// $whereType = count($selector->fields()) > 1 ? 'OR' : 'AND';
$zeroIsEmpty = $ft->isEmptyValue($field, "0");
$zeroIsNotEmpty = !$zeroIsEmpty;
$value = (string) $value;
@@ -3577,10 +3616,11 @@ class PageFinder extends Wire {
if(count($fields) > 1) {
// OR fields present
array_shift($fields);
$subfields = array($subfields);
$subfields = array($subfields); // 1. subfields is definitely an array…
foreach($fields as $name) {
if(strpos($name, "$fieldName.") === 0) {
list(,$name) = explode('__owner.', $name);
list(,$name) = explode('__owner.', $name);
/** @var array $subfields 2. …but PhpStorm in PHP8 mode can't tell it's an array without this */
$subfields[] = $name;
} else {
$this->syntaxError(
@@ -3652,7 +3692,7 @@ class PageFinder extends Wire {
* @return array
*
*/
public function getPageArrayData(PageArray $pageArray = null) {
public function getPageArrayData(?PageArray $pageArray = null) {
if($pageArray !== null && count($this->pageArrayData)) {
$pageArray->data($this->pageArrayData);
}
@@ -3743,5 +3783,6 @@ class PageFinder extends Wire {
* @property Selectors $selectors Original Selectors object
* @property DatabaseQuerySelect $parentQuery Parent database query
* @property PageFinder $pageFinder PageFinder instance that initiated the query
* @property string $joinType Value 'join', 'leftjoin', or '' (if not yet known), can be overridden (3.0.237+)
*/
abstract class PageFinderDatabaseQuerySelect extends DatabaseQuerySelect { }

View File

@@ -9,7 +9,7 @@
* Except where indicated, please treat these properties as private to the
* Page class.
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
*/
@@ -75,6 +75,7 @@ abstract class PageProperties {
'addable' => 'm',
'child' => 'm',
'children' => 'm',
'cloneable' => 'm',
'created' => 's',
'createdStr' => '',
'createdUser' => '',
@@ -137,6 +138,7 @@ abstract class PageProperties {
'rootParent' => 'm',
'siblings' => 'm',
'sort' => 's',
'sortPrevious' => 'p',
'sortable' => 'm',
'sortfield' => 's',
'status' => 's',

View File

@@ -624,7 +624,7 @@ class PageTraversal {
'host' => '',
'pageNum' => is_int($options) || (is_string($options) && in_array($options, array('+', '-'))) ? $options : 1,
'data' => array(),
'urlSegmentStr' => is_string($options) ? $options : '',
'urlSegmentStr' => (is_string($options) && !in_array($options, array('+', '-'))) ? $options : '',
'urlSegments' => array(),
'language' => is_object($options) && wireInstanceOf($options, 'Language') ? $options : null,
);
@@ -707,7 +707,7 @@ class PageTraversal {
}
if(!strlen($prefix)) $prefix = $config->pageNumUrlPrefix;
$url = rtrim($url, '/') . '/' . $prefix . ((int) $options['pageNum']);
if($template->slashPageNum) $url .= '/';
if(((int) $template->slashPageNum) === 1) $url .= '/';
}
}
@@ -852,6 +852,7 @@ class PageTraversal {
* - `http` (bool): True to force scheme and hostname in URL (default=auto detect).
* - `language` (Language|bool): Optionally specify Language to start editor in, or boolean true to force current user language.
* - `find` (string): Name of field to find in the editor (3.0.151+)
* - `vars` (array): Additional variables to include in query string (3.0.239+)
* @return string URL for editing this page
*
*/
@@ -862,6 +863,7 @@ class PageTraversal {
$https = $adminTemplate && ($adminTemplate->https > 0) && !$config->noHTTPS;
$url = ($https && !$config->https) ? 'https://' . $config->httpHost : '';
$url .= $config->urls->admin . "page/edit/?id=$page->id";
$optionsArray = is_array($options) ? $options : array();
if($options === true || (is_array($options) && !empty($options['http']))) {
if(strpos($url, '://') === false) {
@@ -872,15 +874,22 @@ class PageTraversal {
$languages = $page->wire()->languages;
if($languages) {
$language = $page->wire()->user->language;
if(empty($options['language'])) {
if(empty($optionsArray['language'])) {
if($page->wire()->page->template->id == $adminTemplate->id) $language = null;
} else if($options['language'] instanceof Page) {
$language = $options['language'];
} else if($options['language'] !== true) {
$language = $languages->get($options['language']);
} else if($optionsArray['language'] instanceof Page) {
$language = $optionsArray['language'];
} else if($optionsArray['language'] !== true) {
$language = $languages->get($optionsArray['language']);
}
if($language && $language->id) $url .= "&language=$language->id";
}
$version = (int) ((string) $page->get('_version|_repeater_version'));
if($version) $url .= "&version=$version";
if(!empty($optionsArray['vars'])) {
$url .= '&' . http_build_query($optionsArray['vars']);
}
$append = $page->wire()->session->getFor($page, 'appendEditUrl');
@@ -1138,11 +1147,11 @@ class PageTraversal {
*
* @param Page $page
* @param string|array $selector Optional selector. When specified, will find nearest next sibling that matches.
* @param PageArray $siblings Optional siblings to use instead of the default. May also be specified as first argument when no selector needed.
* @param PageArray|null $siblings Optional siblings to use instead of the default. May also be specified as first argument when no selector needed.
* @return Page|NullPage Returns the next sibling page, or a NullPage if none found.
*
*/
public function nextSibling(Page $page, $selector = '', PageArray $siblings = null) {
public function nextSibling(Page $page, $selector = '', ?PageArray $siblings = null) {
if($selector instanceof PageArray) {
// backwards compatible to when $siblings was first argument
$siblings = $selector;
@@ -1182,11 +1191,11 @@ class PageTraversal {
*
* @param Page $page
* @param string|array $selector Optional selector. When specified, will find nearest previous sibling that matches.
* @param PageArray $siblings Optional siblings to use instead of the default. May also be specified as first argument when no selector needed.
* @param PageArray|null $siblings Optional siblings to use instead of the default. May also be specified as first argument when no selector needed.
* @return Page|NullPage Returns the previous sibling page, or a NullPage if none found.
*
*/
public function prevSibling(Page $page, $selector = '', PageArray $siblings = null) {
public function prevSibling(Page $page, $selector = '', ?PageArray $siblings = null) {
if($selector instanceof PageArray) {
// backwards compatible to when $siblings was first argument
$siblings = $selector;
@@ -1213,11 +1222,11 @@ class PageTraversal {
*
* @param Page $page
* @param string|array $selector Optional selector. When specified, will filter the found siblings.
* @param PageArray $siblings Optional siblings to use instead of the default.
* @param PageArray|null $siblings Optional siblings to use instead of the default.
* @return PageArray Returns all matching pages after this one.
*
*/
public function nextAllSiblings(Page $page, $selector = '', PageArray $siblings = null) {
public function nextAllSiblings(Page $page, $selector = '', ?PageArray $siblings = null) {
if(is_null($siblings)) {
$siblings = $page->parent()->children();
@@ -1247,11 +1256,11 @@ class PageTraversal {
*
* @param Page $page
* @param string|array $selector Optional selector. When specified, will filter the found siblings.
* @param PageArray $siblings Optional siblings to use instead of the default.
* @param PageArray|null $siblings Optional siblings to use instead of the default.
* @return PageArray
*
*/
public function prevAllSiblings(Page $page, $selector = '', PageArray $siblings = null) {
public function prevAllSiblings(Page $page, $selector = '', ?PageArray $siblings = null) {
if(is_null($siblings)) {
$siblings = $page->parent()->children();
@@ -1282,7 +1291,7 @@ class PageTraversal {
* @return PageArray
*
*/
public function nextUntilSiblings(Page $page, $selector = '', $filter = '', PageArray $siblings = null) {
public function nextUntilSiblings(Page $page, $selector = '', $filter = '', ?PageArray $siblings = null) {
if(is_null($siblings)) {
$siblings = $page->parent()->children();
@@ -1334,7 +1343,7 @@ class PageTraversal {
* @return PageArray
*
*/
public function prevUntilSiblings(Page $page, $selector = '', $filter = '', PageArray $siblings = null) {
public function prevUntilSiblings(Page $page, $selector = '', $filter = '', ?PageArray $siblings = null) {
if(is_null($siblings)) {
$siblings = $page->parent()->children();

View File

@@ -879,7 +879,7 @@ class PageValues extends Wire {
$template = $page->template();
if(!$template) return $page->_parentGet($key);
$field = $this->getField($page, $key);
$field = $page->getField($key);
$value = $page->_parentGet($key);
if(!$field) return $value; // likely a runtime field, not part of our data
@@ -1021,7 +1021,16 @@ class PageValues extends Wire {
public function setFieldValue(Page $page, $key, $value, $load = true) {
if(!$page->template()) {
throw new WireException("You must assign a template to the page before setting field values ($key)");
$config = $page->wire()->config;
$name = strpos($key, '__') ? substr($key, 0, strpos($key, '__')) : $key;
$error = "You must assign a template to page $page before setting '$name' field.";
if($config->debug) {
// allow page to proceed in debug mode so that it's possible to delete it if needed
$page->error($error);
$page->template($page->wire()->pages->get($config->http404PageID)->template);
} else {
throw new WireException($error);
}
}
$isLoaded = $page->isLoaded();
@@ -1038,7 +1047,7 @@ class PageValues extends Wire {
}
// check if the given key resolves to a Field or not
$field = $this->getField($page, $key);
$field = $page->getField($key);
if(!$field) {
// not a known/saveable field, let them use it for runtime storage
$valPrevious = $page->_parentGet($key);

View File

@@ -12,7 +12,7 @@
* Pagefile objects are contained by a `Pagefiles` object.
* #pw-body
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @property-read string $url URL to the file on the server.
@@ -357,7 +357,7 @@ class Pagefile extends WireData implements WireArrayItem {
$key = $type === 'created' ? '_createdUser' : '_modifiedUser';
if(!$this->$key) {
$id = (int) parent::get($type . '_users_id');
$this->$key = $id ? $this->wire('users')->get($id) : new NullPage();
$this->$key = ($id ? $this->wire()->users->get($id) : new NullPage());
}
return $this->$key;
}
@@ -421,11 +421,11 @@ class Pagefile extends WireData implements WireArrayItem {
* Set a description, optionally parsing JSON language-specific descriptions to separate properties
*
* @param string|array $value
* @param Page|Language Langage to set it for. Omit to determine automatically.
* @param Language|null Langage to set it for. Omit to determine automatically.
* @return $this
*
*/
protected function setDescription($value, Page $language = null) {
protected function setDescription($value, ?Page $language = null) {
$languages = $this->wire()->languages;
@@ -577,7 +577,7 @@ class Pagefile extends WireData implements WireArrayItem {
if(is_null($language)) {
// return description for current user language, or inherit from default if not available
$user = $this->wire('user');
$user = $this->wire()->user;
$value = null;
if($user->language && $user->language->id) {
$value = parent::get("description{$user->language}");
@@ -707,6 +707,7 @@ class Pagefile extends WireData implements WireArrayItem {
$value = $this->uploadName();
break;
default:
if(strpos($key, '|')) return parent::get($key);
$value = $this->getFieldValue($key);
}
@@ -1386,12 +1387,12 @@ class Pagefile extends WireData implements WireArrayItem {
* #pw-internal
*
* @param string $name
* @param PagefileExtra $value
* @param PagefileExtra|null $value
* @return PagefileExtra[]|PagefileExtra|null
* @since 3.0.132
*
*/
public function extras($name = null, PagefileExtra $value = null) {
public function extras($name = null, ?PagefileExtra $value = null) {
if($name === null) return $this->extras;
if($value instanceof PagefileExtra) {
$this->extras[$name] = $value;
@@ -1459,6 +1460,45 @@ class Pagefile extends WireData implements WireArrayItem {
return true;
}
/**
* Get all filenames associated with this file
*
* @return array
* @since 3.0.233
*
*/
public function getFiles() {
$filename = $this->filename();
$filenames = array($filename);
foreach($this->extras() as $extra) {
if($extra->exists()) $filenames[] = $extra->filename();
}
return $filenames;
}
/**
* Get or set hidden state of this file
*
* Files that are hidden do not appear in the formatted field value,
* but do appear in the unformatted value.
*
* @param bool|null $set
* @since 3.0.237
*
*/
public function hidden($set = null) {
$value = (bool) $this->filedata('_hide');
if($set === null || $set === $value) return $value;
if($set === false) {
$this->filedata(false, '_hide');
} else if($set === true) {
$this->filedata('_hide', true);
} else {
throw new WireException('Invalid arg for Pagefile::hidden(arg)');
}
return $set;
}
/**
* Ensures that isset() and empty() work for dynamic class properties

View File

@@ -38,7 +38,7 @@
* Typically a Pagefiles object will be associated with a specific field attached to a Page.
* There may be multiple instances of Pagefiles attached to a given Page (depending on what fields are in it's fieldgroup).
*
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*
@@ -124,6 +124,8 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
public function __construct(Page $page) {
$this->setPage($page);
parent::__construct();
$this->usesNumericKeys = false;
$this->indexedByName = true;
}
/**
@@ -304,13 +306,13 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
/**
* Get for direct access to properties
*
* @param int|string $property
* @param int|string $name
* @return bool|mixed|Page|Wire|WireData
*
*/
public function __get($property) {
if(in_array($property, array('page', 'field', 'url', 'path'))) return $this->get($property);
return parent::__get($property);
public function __get($name) {
if(in_array($name, array('page', 'field', 'url', 'path'))) return $this->get($name);
return parent::__get($name);
}
/**
@@ -375,7 +377,9 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
*/
public function hookPageSave() {
if($this->page && $this->field && !$this->page->isChanged($this->field->name)) return $this;
if($this->page && $this->field) {
if(!$this->page->isChanged($this->field->name)) return $this;
}
$this->page->filesManager()->uncache();
@@ -595,6 +599,7 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
}
}
$basename = strtolower($basename);
if(!ctype_alnum(ltrim($ext, '.'))) $ext = preg_replace('/[^a-z0-9.]/', '_', $ext);
if(!$allowDots && strpos($basename, '.') !== false) $basename = str_replace('.', '_', $basename);
$basename .= $ext;
@@ -780,12 +785,13 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
if(!is_bool($set)) {
// temp status is not being set
if(!$isTemp) return false; // if not a temp file, we can exit now
if(!$checkDeletable) return $isTemp; // if not checking deletable, we can exit now
if(!$checkDeletable) return true; // if not checking deletable, we can exit now
}
$user = $this->wire('user');
$user = $this->wire()->user;
$session = $this->wire()->session;
$now = time();
$session = $this->wire('session');
$pageID = $this->page ? $this->page->id : 0;
$fieldID = $this->field ? $this->field->id : 0;
$sessionKey = "tempFiles_{$pageID}_{$fieldID}";
@@ -806,8 +812,11 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
unset($tempFiles[$pagefile->basename]);
// remove file from session - note that this means a 'deletable' check can only be used once, for newly uploaded files
// as it is assumed you will be removing the file as a result of this method call
if(count($tempFiles)) $session->set($this, $sessionKey, $tempFiles);
else $session->remove($this, $sessionKey);
if(count($tempFiles)) {
$session->set($this, $sessionKey, $tempFiles);
} else {
$session->remove($this, $sessionKey);
}
}
}
@@ -867,7 +876,11 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
}
if(count($removed) && $this->page && $this->field) {
$this->page->save($this->field->name, array('quiet' => true));
$this->message("Removed '{$this->field->name}' temp file(s) for page {$this->page->path} - " . implode(', ', $removed), Notice::debug | Notice::log);
$this->message(
"Removed '{$this->field->name}' temp file(s) for page {$this->page->path} - " .
implode(', ', $removed),
Notice::debug | Notice::log
);
}
return count($removed);
}
@@ -979,6 +992,22 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
return $fieldtype->getFieldsPage($field);
}
/**
* Get all filenames associated with this Pagefiles object
*
* @return array
* @since 3.0.233
*
*/
public function getFiles() {
$filenames = array();
foreach($this as $pagefile) {
/** @var Pagefile $pagefile */
$filenames = array_merge($filenames, $pagefile->getFiles());
}
return $filenames;
}
/**
* Debug info
*

View File

@@ -154,7 +154,7 @@ class Pageimage extends Pagefile {
* $pageimage = new Pageimage($page->images, '/path/to/file.png');
* ~~~~~
*
* @param Pageimages|Pagefiles $pagefiles
* @param Pagefiles $pagefiles
* @param string $filename Full path and filename to this pagefile
* @throws WireException
*
@@ -543,10 +543,17 @@ class Pageimage extends Pagefile {
if(!$filename) $filename = $this->filename;
$xml = @file_get_contents($filename);
if($xml) {
$a = @simplexml_load_string($xml)->attributes();
if($xml && false !== ($a = @simplexml_load_string($xml))) {
$a = $a->attributes();
if((int) $a->width > 0) $width = (int) $a->width;
if((int) $a->height > 0) $height = (int) $a->height;
if((!$width || !$height) && $a->viewBox) {
$values = explode(' ', $a->viewBox);
if(count($values) === 4) {
$width = (int) round($values[2]);
$height = (int) round($values[3]);
}
}
}
if((!$width || !$height) && (extension_loaded('imagick') || class_exists('\IMagick'))) {
@@ -1702,17 +1709,26 @@ class Pageimage extends Pagefile {
/**
* Get WebP "extra" version of this Pageimage
*
* @param array $webpOptions Optionally override certain defaults from `$config->webpOptions` (requires 3.0.229+):
* - `useSrcUrlOnSize` (bool): Fallback to source file URL when webp file is larger than source? (default=true)
* - `useSrcUrlOnFail` (bool): Fallback to source file URL when webp file fails for some reason? (default=true)
* - `quality' (int): Quality setting of 1-100 where higher is better but larger in file size (default=90)
* Note that his quality setting is only used if the .webp file does not already exist.
* @return PagefileExtra
* @since 3.0.132
*
*/
public function webp() {
public function webp(array $webpOptions = array()) {
$webp = $this->extras('webp');
if(!$webp) {
$webp = new PagefileExtra($this, 'webp');
$webp->setArray($this->wire()->config->webpOptions);
$webpOptions = array_merge($this->wire()->config->webpOptions, $webpOptions);
$webp->setArray($webpOptions);
$this->extras('webp', $webp);
$webp->addHookAfter('create', $this, 'hookWebpCreate');
} else if(count($webpOptions)) {
/** @var PagefileExtra $webp */
$webp->setArray($webpOptions);
}
return $webp;
}
@@ -1747,6 +1763,8 @@ class Pageimage extends Pagefile {
$width = $this->width;
$height = 0;
}
$quality = (int) $webp->get('quality');
if($quality > 0) $options['webpQuality'] = $quality;
$options['webpAdd'] = true;
try {
$original->size($width, $height, $options);
@@ -1764,12 +1782,12 @@ class Pageimage extends Pagefile {
* #pw-internal
*
* @param string $name
* @param PagefileExtra $value
* @param PagefileExtra|null $value
* @return PagefileExtra[]
* @since 3.0.132
*
*/
public function extras($name = null, PagefileExtra $value = null) {
public function extras($name = null, ?PagefileExtra $value = null) {
if($name) return parent::extras($name, $value);
$extras = parent::extras();
$extras['webp'] = $this->webp();
@@ -1834,6 +1852,28 @@ class Pageimage extends Pagefile {
return parent::__isset($key);
}
/**
* Get all filenames associated with this image
*
* @return array
* @since 3.0.233
*
*/
public function getFiles() {
$filenames = parent::getFiles();
foreach($this->extras() as $extra) {
if($extra->exists()) $filenames[] = $extra->filename();
}
foreach($this->getVariations() as $pagefile) {
/** @var Pagefile $pagefile */
$filenames[] = $pagefile->filename();
foreach($pagefile->extras() as $extra) {
if($extra->exists()) $filenames[] = $extra->filename();
}
}
return $filenames;
}
/**
* Basic debug info
*
@@ -1873,4 +1913,3 @@ class Pageimage extends Pagefile {
}
}

View File

@@ -8,7 +8,7 @@
*
* This is the most used object in the ProcessWire API.
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* @link http://processwire.com/api/variables/pages/ Offical $pages Documentation
@@ -39,8 +39,9 @@
* HOOKABLE METHODS
* ================
* @method PageArray find($selectorString, array $options = array()) Find and return all pages matching the given selector string. Returns a PageArray. #pw-group-retrieval
* @method bool save(Page $page, $options = array()) Save any changes made to the given $page. Same as : $page->save() Returns true on success. #pw-group-manipulation
* @method bool save(Page $page, $options = array()) Save any changes made to the given $page. Same as $page->save(); Returns true on success. #pw-group-manipulation
* @method bool saveField(Page $page, $field, array $options = array()) Save just the named field from $page. Same as: $page->save('field') #pw-group-manipulation
* @method array saveFields(Page $page, $fields, array $options = array()) Saved multiple named fields for $page. @since 3.0.242 #pw-group-manipulation
* @method bool trash(Page $page, $save = true) Move a page to the trash. If you have already set the parent to somewhere in the trash, then this method won't attempt to set it again. #pw-group-manipulation
* @method bool restore(Page $page, $save = true) Restore a trashed page to its original location. #pw-group-manipulation
* @method int|array emptyTrash(array $options = array()) Empty the trash and return number of pages deleted. #pw-group-manipulation
@@ -62,10 +63,12 @@
* @method saveReady(Page $page) Hook called just before a page is saved.
* @method saved(Page $page, array $changes = array(), $values = array()) Hook called after a page is successfully saved.
* @method added(Page $page) Hook called when a new page has been added.
* @method moveReady(Page $page) Hook called when a page is about to be moved to another parent.
* @method moved(Page $page) Hook called when a page has been moved from one parent to another.
* @method templateChanged(Page $page) Hook called when a page template has been changed.
* @method trashReady(Page $page) Hook called when a page is about to be moved to the trash.
* @method trashed(Page $page) Hook called when a page has been moved to the trash.
* @method restoreReady(Page $page) Hook called when a page is about to be restored out of the trash.
* @method restored(Page $page) Hook called when a page has been moved OUT of the trash.
* @method deleteReady(Page $page, array $options) Hook called just before a page is deleted.
* @method deleted(Page $page, array $options) Hook called after a page has been deleted.
@@ -73,6 +76,7 @@
* @method deletedBranch(Page $page, array $options, $numDeleted) Hook called after branch of pages deleted, on initiating page only.
* @method cloneReady(Page $page, Page $copy) Hook called just before a page is cloned.
* @method cloned(Page $page, Page $copy) Hook called after a page has been successfully cloned.
* @method renameReady(Page $page) Hook called when a page is about to be renamed.
* @method renamed(Page $page) Hook called after a page has been successfully renamed.
* @method sorted(Page $page, $children = false, $total = 0) Hook called after $page has been sorted.
* @method statusChangeReady(Page $page) Hook called when a page's status has changed and is about to be saved.
@@ -87,9 +91,6 @@
* @method savedPageOrField(Page $page, array $changes) Hook inclusive of both saved() and savedField().
* @method found(PageArray $pages, array $details) Hook called at the end of a $pages->find().
*
* TO-DO
* =====
* @todo Update saveField to accept array of field names as an option.
*
*/
@@ -263,7 +264,7 @@ class Pages extends Wire {
* - `findOne` (bool): Apply optimizations for finding a single page (default=false).
* - `findAll` (bool): Find all pages with no exclusions, same as "include=all" option (default=false).
* - `findIDs` (bool|int): 1 to get array of page IDs, true to return verbose array, 2 to return verbose array with all cols in 3.0.153+. (default=false).
* - `getTotal` (bool): Whether to set returning PageArray's "total" property (default=true, except when findOne=true).
* - `getTotal` (bool): Whether to set returning PageArray's "total" property (default=true) except when findOne=true.
* - `loadPages` (bool): Whether to populate the returned PageArray with found pages (default=true).
* The only reason why you'd want to change this to false would be if you only needed the count details from
* the PageArray: getTotal(), getStart(), getLimit, etc. This is intended as an optimization for $pages->count().
@@ -756,8 +757,8 @@ class Pages extends Wire {
}
if(!empty($options['parent_id'])) {
unset($options['parent_id']);
$parent_id = (int) $options['parent_id'];
unset($options['parent_id']);
} else if($parent) {
unset($options['parent']);
if($parent instanceof Page) {
@@ -867,6 +868,38 @@ class Pages extends Wire {
return $this->editor()->saveField($page, $field, $options);
}
/**
* Save multiple named fields from given page
*
* ~~~~~
* // you can specify field names as array…
* $a = $pages->saveFields($page, [ 'title', 'body', 'summary' ]);
*
* // …or a CSV string of field names:
* $a = $pages->saveFields($page, 'title, body, summary');
*
* // return value is array of saved field/property names
* print_r($a); // outputs: array( 'title', 'body', 'summary' )
* ~~~~~
*
* #pw-group-manipulation
*
* @param Page $page Page to save
* @param array|string|string[]|Field[] $fields Array of field names to save or CSV/space separated field names to save.
* These should only be Field names and not native page property names.
* @param array|string $options Optionally specify one or more of the following to modify default behavior:
* - `quiet` (boolean): Specify true to bypass updating of modified user and time (default=false).
* - `noHooks` (boolean): Prevent before/after save hooks (default=false), please also use $pages->___saveField() for call.
* - See $options argument for Pages::save() for additional options
* @return array Array of saved field names (may also include property names if they were modified)
* @throws WireException
* @since 3.0.242
*
*/
public function ___saveFields(Page $page, $fields, array $options = array()) {
return $this->editor()->saveFields($page, $fields, $options);
}
/**
* Add a new page using the given template and parent
*
@@ -1013,7 +1046,7 @@ class Pages extends Wire {
* @throws WireException|\Exception on fatal error
*
*/
public function ___clone(Page $page, Page $parent = null, $recursive = true, $options = array()) {
public function ___clone(Page $page, ?Page $parent = null, $recursive = true, $options = array()) {
return $this->editor()->_clone($page, $parent, $recursive, $options);
}
@@ -1395,6 +1428,7 @@ class Pages extends Wire {
* - Assigns a 'sort' value'.
*
* #pw-internal
* #pw-group-manipulation
*
* @param Page $page
*
@@ -1412,6 +1446,7 @@ class Pages extends Wire {
* already have a name, unless the name is "untitled"
*
* #pw-internal
* #pw-group-manipulation
*
* @param Page $page
* @param array $options
@@ -1655,13 +1690,13 @@ class Pages extends Wire {
*
* #pw-group-cache
*
* @param Page $page Optional Page that initiated the uncacheAll
* @param Page|null $page Optional Page that initiated the uncacheAll
* @param array $options Options to modify default behavior:
* - `shallow` (bool): By default, this method also calls $page->uncache(). To prevent that call, set this to true.
* @return int Number of pages uncached
*
*/
public function uncacheAll(Page $page = null, array $options = array()) {
public function uncacheAll(?Page $page = null, array $options = array()) {
return $this->cacher->uncacheAll($page, $options);
}
@@ -1915,6 +1950,11 @@ class Pages extends Wire {
$class = empty($options['pageClass']) ? 'Page' : $options['pageClass'];
unset($options['template'], $options['parent'], $options['pageClass']);
if($template && !$template instanceof Template) {
$template = $this->wire()->templates->get($template);
if(!$template instanceof Template) $template = null;
}
if(strpos($class, "\\") === false) $class = wireClassName($class, true);
@@ -2198,6 +2238,20 @@ class Pages extends Wire {
$page->setQuietly('_added', true);
}
/**
* Hook called when a page is about to be moved to another parent
*
* Note the previous parent is accessible in the `$page->parentPrevious` property.
*
* #pw-hooker
*
* @param Page $page Page that is about to be moved.
* @since 3.0.235
*
*/
public function ___moveReady(Page $page) {
}
/**
* Hook called when a page has been moved from one parent to another
*
@@ -2258,6 +2312,18 @@ class Pages extends Wire {
public function ___trashed(Page $page) {
$this->log("Trashed page", $page);
}
/**
* Hook called when a page is about to be moved OUT of the trash (restored)
*
* #pw-hooker
*
* @param Page $page Page that is about to be restored
* @since 3.0.235
*
*/
public function ___restoreReady(Page $page) {
}
/**
* Hook called when a page has been moved OUT of the trash (restored)
@@ -2387,6 +2453,29 @@ class Pages extends Wire {
public function ___cloned(Page $page, Page $copy) {
$this->log("Cloned page to $copy->path", $page);
}
/**
* Hook called when a page is about to be renamed i.e. had its name field change)
*
* The previous name can be accessed at `$page->namePrevious`.
* The new name can be accessed at `$page->name`.
*
* This hook is only called when a page's name changes. It is not called when
* a page is moved unless the name was changed at the same time.
*
* **Multi-language note:**
* Also note this hook may be called if a page's multi-language name changes.
* In those cases the language-specific name is stored in "name123" while the
* previous value is stored in "-name123" (where 123 is the language ID).
*
* #pw-hooker
*
* @param Page $page The $page that was renamed
* @since 3.0.235
*
*/
public function ___renameReady(Page $page) {
}
/**
* Hook called when a page has been renamed (i.e. had its name field change)

View File

@@ -5,7 +5,7 @@
*
* Implements page manipulation methods of the $pages API variable
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
*/
@@ -27,7 +27,7 @@ class PagesEditor extends Wire {
*
*/
protected $pages;
/**
* Construct
*
@@ -353,7 +353,7 @@ class PagesEditor extends Wire {
if(!$parent->id) $parent = $this->pages->get("include=all, template=$idStr");
}
if($parent->id) $page->parent = $parent;
if($parent && $parent->id) $page->parent = $parent;
}
// assign page name
@@ -424,6 +424,9 @@ class PagesEditor extends Wire {
* - `ignoreFamily` (boolean): Bypass check of allowed family/parent settings when saving (default=false)
* - `noHooks` (boolean): Prevent before/after save hooks from being called (default=false)
* - `noFields` (boolean): Bypass saving of custom fields (default=false)
* - `caller` (string): Optional name of calling function (i.e. 'pages.trash'), for internal use (default='') 3.0.235+
* - `callback` (string|callable): Hook method name from $pages or callable to trigger after save.
* It receives a single $page argument. For internal use. (default='') 3.0.235+
* @return bool True on success, false on failure
* @throws WireException
*
@@ -438,6 +441,8 @@ class PagesEditor extends Wire {
'ignoreFamily' => false,
'noHooks' => false,
'noFields' => false,
'caller' => '',
'callback' => '',
);
if(is_string($options)) $options = Selectors::keyValueStringToArray($options);
@@ -445,11 +450,15 @@ class PagesEditor extends Wire {
$user = $this->wire()->user;
$languages = $this->wire()->languages;
$language = null;
$parentPrevious = $page->parentPrevious;
$caller = $options['caller'];
$callback = $options['callback'];
$useHooks = empty($options['noHooks']);
// if language support active, switch to default language so that saved fields and hooks don't need to be aware of language
if($languages && $page->id != $user->id) {
$language = $user->language && $user->language->id ? $user->language : null;
if($language) $user->language = $languages->getDefault();
if($languages && $page->id != $user->id && "$user->language") {
$language = $user->language;
$user->setLanguage($languages->getDefault());
}
$reason = '';
@@ -457,26 +466,42 @@ class PagesEditor extends Wire {
if($isNew) $this->pages->setupNew($page);
if(!$this->isSaveable($page, $reason, '', $options)) {
if($language) $user->language = $language;
throw new WireException("Cant save page {$page->id}: {$page->path}: $reason");
if($language) $user->setLanguage($language);
throw new WireException(rtrim("Cant save page (id=$page->id): $page->path", ": ") . ": $reason");
}
if($page->hasStatus(Page::statusUnpublished) && $page->template->noUnpublish) {
$page->removeStatus(Page::statusUnpublished);
}
if($page->parentPrevious && !$isNew) {
if($page->isTrash() && !$page->parentPrevious->isTrash()) {
$this->pages->trash($page, false);
} else if($page->parentPrevious->isTrash() && !$page->parent->isTrash()) {
$this->pages->restore($page, false);
if($parentPrevious && !$isNew) {
if($useHooks) $this->pages->moveReady($page);
if($caller !== 'pages.trash' && $caller !== 'pages.restore') {
if($page->isTrash() && !$parentPrevious->isTrash()) {
if($this->pages->trash($page, false)) $callback = 'trashed';
} else if($parentPrevious->isTrash() && !$page->parent->isTrash()) {
if($this->pages->restore($page, false)) $callback = 'restored';
}
}
}
if($options['adjustName']) $this->pages->names()->checkNameConflicts($page);
if(!$this->savePageQuery($page, $options)) return false;
$result = $this->savePageFinish($page, $isNew, $options);
if($language) $user->language = $language; // restore language
if($page->namePrevious && !$isNew && $page->namePrevious != $page->name) {
if($useHooks) $this->pages->renameReady($page);
}
$result = $this->savePageQuery($page, $options);
if($result) $result = $this->savePageFinish($page, $isNew, $options);
if($language) $user->setLanguage($language); // restore language
if($result && !empty($callback) && $useHooks) {
if(is_string($callback) && ctype_alnum($callback)) {
$this->pages->$callback($page); // hook method name in $pages
} else if(is_callable($callback)) {
$callback($page); // user defined callback
}
}
return $result;
}
@@ -790,6 +815,11 @@ class PagesEditor extends Wire {
if($page->templatePrevious) $this->pages->templateChanged($page);
if(in_array('status', $changes)) $this->pages->statusChanged($page);
}
if($triggerAddedPage && $page->rootParent()->id === $this->wire()->config->trashPageID) {
// new page created directly in trash, not a great way to start but that's how it is
$this->savePageStatus($page, Page::statusTrash);
}
$this->pages->debugLog('save', $page, true);
@@ -912,6 +942,90 @@ class PagesEditor extends Wire {
return $return;
}
/**
* Save multiple named fields from given page
*
* ~~~~~
* // you can specify field names as array…
* $a = $pages->saveFields($page, [ 'title', 'body', 'summary' ]);
*
* // …or a CSV string of field names:
* $a = $pages->saveFields($page, 'title, body, summary');
*
* // return value is array of saved field/property names
* print_r($a); // outputs: array( 'title', 'body', 'summary' )
* ~~~~~
*
* @param Page $page Page to save
* @param array|string|string[]|Field[] $fields Array of field names to save or CSV/space separated field names to save.
* These should only be Field names and not native page property names.
* @param array|string $options Optionally specify one or more of the following to modify default behavior:
* - `quiet` (boolean): Specify true to bypass updating of modified user and time (default=false).
* - `noHooks` (boolean): Prevent before/after save hooks (default=false), please also use $pages->___saveField() for call.
* - See $options argument for Pages::save() for additional options
* @return array Array of saved field names (may also include property names if they were modified)
* @throws WireException
* @since 3.0.242
*
*/
public function saveFields(Page $page, $fields, array $options = array()) {
$saved = array();
$quiet = !empty($options['quiet']);
$noHooks = !empty($options['noHooks']);
// do not update modified user/time until last save
if(!$quiet) $options['quiet'] = true;
if(!is_array($fields)) {
$fields = explode(' ', str_replace(',', ' ', "$fields"));
}
foreach($fields as $key => $field) {
$field = trim("$field");
if(empty($field) || !$page->hasField($field)) unset($fields[$key]);
}
// save each field
foreach($fields as $field) {
if($noHooks) {
$success = $this->saveField($page, $field, $options);
} else {
$success = $this->pages->saveField($page, $field, $options);
}
if($success) {
$saved[$field] = $field;
$page->untrackChange($field);
}
}
if($quiet) {
// do not save native properties or update page modified-user/modified
} else {
// finish by saving the page without fields
$options['quiet'] = false;
foreach($page->getChanges() as $name) {
if($page->hasField($name)) continue;
// add only changed native properties to saved list
$saved[$name] = $name;
}
$options['noFields'] = true;
if($noHooks) {
$this->save($page, $options);
} else {
$this->pages->save($page, $options);
}
}
$this->pages->debugLog('saveFields', "$page:" . implode(',', $fields), $saved);
return $saved;
}
/**
* Silently add status flag to a Page and save
*
@@ -959,7 +1073,7 @@ class PagesEditor extends Wire {
*
*/
public function saveStatus(Page $page) {
return $this->savePageStatus($page, $page->status) > 0;
return $this->savePageStatus($page, $page->status, false, 2) > 0;
}
/**
@@ -985,29 +1099,34 @@ class PagesEditor extends Wire {
$database = $this->wire()->database;
$rowCount = 0;
$multi = is_array($pageID) || $pageID instanceof PageArray;
$page = $pageID instanceof Page ? $pageID : null;
$status = (int) $status;
if($status < 0 || $status > Page::statusMax) {
throw new WireException("status must be between 0 and " . Page::statusMax);
}
$sql = "UPDATE pages SET status=";
$sqlUpdate = "UPDATE pages SET status=";
if($remove === 2) {
// overwrite status (internal/undocumented)
$sql .= "status=$status";
$sqlUpdate .= "status=$status";
if($page instanceof Page) $page->status = $status;
} else if($remove) {
// remove status
$sql .= "status & ~$status";
$sqlUpdate .= "status & ~$status";
if($page instanceof Page) $page->removeStatus($status);
} else {
// add status
$sql .= "status|$status";
$sqlUpdate .= "status|$status";
if($page instanceof Page) $page->addStatus($status);
}
if($multi && $recursive) {
// multiple page IDs combined with recursive option, must be handled individually
foreach($pageID as $id) {
$rowCount += $this->savePageStatus((int) "$id", $status, $recursive, $remove);
$id = $id instanceof Page ? $id : (int) "$id";
$rowCount += $this->savePageStatus($id, $status, $recursive, $remove);
}
// exit early in this case
return $rowCount;
@@ -1019,15 +1138,17 @@ class PagesEditor extends Wire {
$id = (int) "$id";
if($id > 0) $ids[$id] = $id;
}
if(!count($ids)) $ids[] = 0;
$query = $database->prepare("$sql WHERE id IN(" . implode(',', $ids) . ")");
$database->execute($query);
return $query->rowCount();
if(count($ids)) {
$query = $database->prepare("$sqlUpdate WHERE id IN(" . implode(',', $ids) . ")");
$database->execute($query);
$rowCount = $query->rowCount();
}
return $rowCount;
} else {
// single page ID or Page object
$pageID = (int) "$pageID";
$query = $database->prepare("$sql WHERE id=:page_id");
$query = $database->prepare("$sqlUpdate WHERE id=:page_id");
$query->bindValue(":page_id", $pageID, \PDO::PARAM_INT);
$database->execute($query);
$rowCount = $query->rowCount();
@@ -1037,12 +1158,13 @@ class PagesEditor extends Wire {
// recursive mode assumed from this point forward
$parentIDs = array($pageID);
$ids = [];
do {
$parentID = array_shift($parentIDs);
// update all children to have the same status
$query = $database->prepare("$sql WHERE parent_id=:parent_id");
$query = $database->prepare("$sqlUpdate WHERE parent_id=:parent_id");
$query->bindValue(":parent_id", $parentID, \PDO::PARAM_INT);
$database->execute($query);
$rowCount += $query->rowCount();
@@ -1062,18 +1184,24 @@ class PagesEditor extends Wire {
/** @noinspection PhpAssignmentInConditionInspection */
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
$parentIDs[] = (int) $row['id'];
$id = (int) $row['id'];
$parentIDs[$id] = $id;
$ids[$id] = $id;
}
$query->closeCursor();
} while(count($parentIDs));
if(count($ids)) {
$rowCount += $this->savePageStatus($ids, $status, false, $remove);
}
return $rowCount;
}
/**
* Permanently delete a page and it's fields.
* Permanently delete a page and its fields.
*
* Unlike trash(), pages deleted here are not restorable.
*
@@ -1096,19 +1224,35 @@ class PagesEditor extends Wire {
'uncacheAll' => false,
'recursive' => is_bool($recursive) ? $recursive : false,
// internal use properties:
'_level' => 0,
// internal recursion level: incremented only by delete operations initiated by this method
'_level' => 0,
// internal delete branch: Page object when deleting a branch
'_deleteBranch' => false,
);
// page IDs for all delete operations, cleared out once no longer recursive
static $deleted = array();
// external recursion level: all recursive delete operations including those initiated from hooks
static $level = 0;
if(is_array($recursive)) $options = $recursive;
$options = array_merge($defaults, $options);
// check if page already deleted in a recursive call
if(isset($deleted[$page->id])) {
// page already deleted, return result from that call
return $options['recursive'] ? $deleted[$page->id] : true;
}
$this->isDeleteable($page, true); // throws WireException
$numDeleted = 0;
$numChildren = $page->numChildren;
$deleteBranch = false;
$level++;
if($numChildren) {
if($numChildren) try {
if(!$options['recursive']) {
throw new WireException("Can't delete Page $page because it has one or more children.");
}
@@ -1119,17 +1263,21 @@ class PagesEditor extends Wire {
}
foreach($page->children('include=all') as $child) {
/** @var Page $child */
if(isset($deleted[$child->id])) continue;
$options['_level']++;
$result = $this->pages->delete($child, true, $options);
$options['_level']--;
if(!$result) throw new WireException("Error doing recursive page delete, stopped by page $child");
$numDeleted += $result;
}
} catch(\Exception $e) {
$level = 0;
$deleted = array();
throw $e;
}
// trigger a hook to indicate delete is ready and WILL occur
$this->pages->deleteReady($page, $options);
$this->clear($page);
$database = $this->wire()->database;
@@ -1140,10 +1288,20 @@ class PagesEditor extends Wire {
$this->pages->sortfields()->delete($page);
$page->setTrackChanges(false);
$page->status = Page::statusDeleted; // no need for bitwise addition here, as this page is no longer relevant
$this->pages->deleted($page, $options);
$numDeleted++;
$deleted[$page->id] = $numDeleted;
$this->pages->deleted($page, $options);
if($deleteBranch) $this->pages->deletedBranch($page, $options, $numDeleted);
if($options['uncacheAll']) $this->pages->uncacheAll($page);
if($level > 0) $level--;
if($level < 1) {
// back at root call, reset all tracking
$deleted = array();
$level = 0;
}
$this->pages->debugLog('delete', $page, true);
return $options['recursive'] ? $numDeleted : true;
@@ -1153,7 +1311,7 @@ class PagesEditor extends Wire {
* Clone an entire page (including fields, file assets, and optionally children) and return it.
*
* @param Page $page Page that you want to clone
* @param Page $parent New parent, if different (default=same parent)
* @param Page|null $parent New parent, if different (default=same parent)
* @param bool $recursive Clone the children too? (default=true)
* @param array|string $options Optional options that can be passed to clone or save
* - forceID (int): force a specific ID
@@ -1163,7 +1321,7 @@ class PagesEditor extends Wire {
* @throws WireException|\Exception on fatal error
*
*/
public function _clone(Page $page, Page $parent = null, $recursive = true, $options = array()) {
public function _clone(Page $page, ?Page $parent = null, $recursive = true, $options = array()) {
$defaults = array(
'forceID' => 0,

View File

@@ -18,7 +18,7 @@
*
* Note: all the "change" prefix options require update=true.
*
* ProcessWire 3.x, Copyright 2017 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
*/
@@ -35,9 +35,9 @@ class PagesExportImport extends Wire {
*/
public function getExportPath($subdir = '') {
/** @var WireFileTools $files */
$files = $this->wire('files');
$path = $this->wire('config')->paths->assets . 'backups/' . $this->className() . '/';
$files = $this->wire()->files;
$config = $this->wire()->config;
$path = $config->paths->assets . 'backups/' . $this->className() . '/';
$readmeText = "When this file is present, files and directories in here are auto-deleted after a short period of time.";
$readmeFile = $this->className() . '.txt';
@@ -75,8 +75,7 @@ class PagesExportImport extends Wire {
*/
public function cleanupFiles($maxAge = 3600) {
/** @var WireFileTools $files */
$files = $this->wire('files');
$files = $this->wire()->files;
$path = $this->getExportPath();
$qty = 0;
@@ -111,13 +110,12 @@ class PagesExportImport extends Wire {
*
* @param PageArray $items
* @param array $options
* @return string|bool Path+filename to ZIP file or boolean false on failure
* @return string Path+filename to ZIP file
*
*/
public function exportZIP(PageArray $items, array $options = array()) {
/** @var WireFileTools $files */
$files = $this->wire('files');
$files = $this->wire()->files;
$options['exportTarget'] = 'zip';
$zipPath = $this->getExportPath();
@@ -173,7 +171,7 @@ class PagesExportImport extends Wire {
$path = $tempDir->get();
$options['filesPath'] = $path;
$zipFileItems = $this->wire('files')->unzip($filename, $path);
$zipFileItems = $this->wire()->files->unzip($filename, $path);
if(empty($zipFileItems)) return false;
@@ -192,7 +190,7 @@ class PagesExportImport extends Wire {
*
* @param PageArray $items
* @param array $options
* @return string|bool JSON string of pages or boolean false on error
* @return string JSON string of pages
*
*/
public function exportJSON(PageArray $items, array $options = array()) {
@@ -232,8 +230,9 @@ class PagesExportImport extends Wire {
*/
public function pagesToArray(PageArray $items, array $options = array()) {
/** @var Config $config */
$config = $this->wire('config');
$config = $this->wire()->config;
$modules = $this->wire()->modules;
$fields = $this->wire()->fields;
$defaults = array(
'verbose' => false,
@@ -247,7 +246,7 @@ class PagesExportImport extends Wire {
'type' => 'ProcessWire:PageArray',
'created' => date('Y-m-d H:i:s'),
'version' => $config->version,
'user' => $this->wire('user')->name,
'user' => $this->wire()->user->name,
'host' => $config->httpHost,
'pages' => array(),
'fields' => array(),
@@ -260,7 +259,7 @@ class PagesExportImport extends Wire {
);
if($items->getLimit()) {
$pageNum = $this->wire('input')->pageNum;
$pageNum = $this->wire()->input->pageNum;
$a['pagination'] = array(
'start' => $items->getStart(),
'limit' => $items->getLimit(),
@@ -273,8 +272,7 @@ class PagesExportImport extends Wire {
unset($a['pagination']);
}
/** @var Languages $languages */
$languages = $this->wire('languages');
$languages = $this->wire()->languages;
if($languages) $languages->setDefault();
$templates = array();
@@ -293,9 +291,9 @@ class PagesExportImport extends Wire {
}
foreach($fieldNames as $fieldName) {
if(isset($a['fields'][$fieldName])) continue;
$field = $this->wire('fields')->get($fieldName);
$field = $fields->get($fieldName);
if(!$field || !$field->type) continue;
$moduleInfo = $this->wire('modules')->getModuleInfoVerbose($field->type);
$moduleInfo = $modules->getModuleInfoVerbose($field->type);
if($options['verbose']) {
$fieldData = $field->getExportData();
unset($fieldData['name']);
@@ -363,8 +361,8 @@ class PagesExportImport extends Wire {
$of = $page->of();
$page->of(false);
/** @var Languages $languages */
$languages = $this->wire('languages');
/** @var Languages|Language[] $languages */
$languages = $this->wire()->languages;
if($languages) $languages->setDefault();
$numFiles = 0;
@@ -457,7 +455,7 @@ class PagesExportImport extends Wire {
*
* @param array $a
* @param array $options
* @return PageArray|bool
* @return PageArray|int
* @throws WireException
*
*/
@@ -476,7 +474,7 @@ class PagesExportImport extends Wire {
if(!empty($options['pageArray']) && $options['pageArray'] instanceof PageArray) {
$pageArray = $options['pageArray'];
} else {
$pageArray = $this->wire('pages')->newPageArray();
$pageArray = $this->wire()->pages->newPageArray();
}
$count = 0;
@@ -494,7 +492,7 @@ class PagesExportImport extends Wire {
foreach($a['pages'] as $item) {
$page = $this->arrayToPage($item, $options);
$id = $item['settings']['id'];
$this->wire('notices')->move($page, $pageArray, array('prefix' => "Page $id: "));
$this->wire()->notices->move($page, $pageArray, array('prefix' => "Page $id: "));
if(!$options['count']) $pageArray->add($page);
$count++;
}
@@ -680,8 +678,7 @@ class PagesExportImport extends Wire {
*/
protected function importGetPage(array &$a, array &$options, array &$errors) {
/** @var Pages $pages */
$pages = $this->wire('pages');
$pages = $this->wire()->pages;
$path = $a['path'];
/** @var Page|NullPage $page */
@@ -738,7 +735,7 @@ class PagesExportImport extends Wire {
if(is_object($template)) {
// ok
} else {
$template = $this->wire('templates')->get($template);
$template = $this->wire()->templates->get($template);
}
if($template) {
$options['template'] = $template;
@@ -762,12 +759,12 @@ class PagesExportImport extends Wire {
// determine parent
static $previousPaths = array();
$usePrevious = true;
$pages = $this->wire('pages');
$pages = $this->wire()->pages;
$path = $a['path'];
if($options['parent']) {
// parent specified in options
if(is_object($options['parent']) && $options['parent'] instanceof Page) {
if($options['parent'] instanceof Page) {
$parent = $options['parent'];
} else if(ctype_digit("$options[parent]")) {
$parent = $pages->get((int) $options['parent']);
@@ -841,7 +838,7 @@ class PagesExportImport extends Wire {
if(!$isNew) $options['changeTemplate'] = false;
$template = $options['template'];
$parent = $options['parent'];
$languages = $this->wire('languages');
$languages = $this->wire()->languages;
$langProperties = array();
// populate page base settings
@@ -980,9 +977,9 @@ class PagesExportImport extends Wire {
$page->trackChange("{$field->name}__");
}
}
if(is_object($pageValue) && $pageValue instanceof Wire) {
if($pageValue instanceof Wire) {
// movie notices from the pageValue to the page
$this->wire('notices')->move($pageValue, $page);
$this->wire()->notices->move($pageValue, $page);
}
} else {
// test import on existing page, avoids actually setting value to the page
@@ -1020,9 +1017,8 @@ class PagesExportImport extends Wire {
// 'file3.gif' => [ ... see above ... ],
// ];
/** @var Pagefiles $pagefiles */
$pagefiles = $page->get($field->name);
if(!$pagefiles || !$pagefiles instanceof Pagefiles) {
if(!$pagefiles instanceof Pagefiles) {
$page->warning("Unable to import files to field '$field->name' because it is not a files field");
return;
}
@@ -1033,7 +1029,7 @@ class PagesExportImport extends Wire {
$variationsAdded = array();
$maxFiles = (int) $field->get('maxFiles');
$languages = $this->wire('languages');
$languages = $this->wire()->languages;
$filesPath = $pagefiles->path();
/** @var null|WireHttp $http */
$http = null;
@@ -1137,7 +1133,7 @@ class PagesExportImport extends Wire {
if($sourceExists) {
// copy variation from options[filesPath]
if($this->wire('files')->copy($sourceFile, $targetFile)) {
if($this->wire()->files->copy($sourceFile, $targetFile)) {
$variationsAdded[] = $name;
} else {
$page->warning("Unable to copy file (image variation): $sourceFile");
@@ -1243,13 +1239,9 @@ class PagesExportImport extends Wire {
$numExisting = 0;
$numNew = 0;
/** @var Pages $pages */
$pages = $this->wire('pages');
/** @var Fields $fields */
$fields = $this->wire('fields');
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
/** @var PageFinder $pageFinder */
$pages = $this->wire()->pages;
$fields = $this->wire()->fields;
$sanitizer = $this->wire()->sanitizer;
$pageFinder = $this->wire(new PageFinder());
// Identify missing fields
@@ -1300,7 +1292,7 @@ class PagesExportImport extends Wire {
// determine which templates are missing, and which fields are missing from templates
foreach($templateNames as $templateName => $fieldNames) {
$template = $this->wire('templates')->get($templateName);
$template = $this->wire()->templates->get($templateName);
if($template) {
// template exists
$missingTemplateFields[$templateName] = array();
@@ -1316,7 +1308,7 @@ class PagesExportImport extends Wire {
}
// determine which parents are missing
foreach($parentPaths as $key => $path) {
foreach($parentPaths as /* $key => */ $path) {
if(isset($pagePaths[$path])) {
// this parent already exists or will be created during import
} else {

View File

@@ -698,7 +698,8 @@ class PagesLoader extends Wire {
foreach($row as $key => $value) {
if(strpos($key, '__')) {
if($value === null) {
$row[$key] = 'null'; // ensure detected by later isset in foreach($joinFields)
// $row[$key] = 'null'; // ensure detected by later isset in foreach($joinFields)
$row[$key] = new NullField();
} else {
$page->setFieldValue($key, $value, false);
}
@@ -712,7 +713,10 @@ class PagesLoader extends Wire {
if(!$template->fieldgroup->hasField($joinField)) continue;
$field = $page->getField($joinField);
if(!$field || !$field->type) continue;
if(isset($row["{$joinField}__data"])) {
$v = isset($row["{$joinField}__data"]) ? $row["{$joinField}__data"] : null;
if($v instanceof NullField) $v = null;
// if(isset($row["{$joinField}__data"])) {
if($v !== null) {
if(!$field->hasFlag(Field::flagAutojoin)) {
$field->addFlag(Field::flagAutojoin);
$tmpAutojoinFields[$field->id] = $field;
@@ -785,25 +789,31 @@ class PagesLoader extends Wire {
$options = array_merge($defaults, $options);
$items = $this->pages->find($selector, $options);
$page = $items->first();
if($page && !$page->viewable(false)) {
if(isset($options['findAll']) && $options['findAll'] === true) {
// page is always allowed through when findAll=true
} else if(isset($options['include']) && $options['include'] === 'all') {
// page is always allowed through when include=all
} else if($page && !$page->viewable(false)) {
// page found but is not viewable, check if include mode was specified and would allow the page
$include = isset($options['include']) ? strtolower($options['include']) : null;
$checkAccess = true;
$selectors = $items->getSelectors();
if($selectors) {
$include = $selectors->getSelectorByField('include');
if($include === null) {
$include = $selectors->getSelectorByField('include');
if($include) $include = strtolower($include->value());
}
$checkAccess = $selectors->getSelectorByField('check_access');
if(!$checkAccess) $checkAccess = $selectors->getSelectorByField('checkAccess');
$checkAccess = $checkAccess ? (bool) $checkAccess->value() : true;
} else {
$include = null;
$checkAccess = true;
}
if(!$include) {
// there was no “include=” selector present
if($checkAccess === true) $page = null;
} else if($include->value() === 'all') {
} else if($include === 'all') {
// allow $page to pass through with include=all mode
} else if($include->value() === 'unpublished' && $page->hasStatus(Page::statusUnpublished) && $checkAccess) {
} else if($include === 'unpublished' && $page->isUnpublished() && $checkAccess) {
// check if user would have access without unpublished status
$status = $page->status;
$page->setQuietly('status', $status & ~Page::statusUnpublished);
@@ -2022,6 +2032,332 @@ class PagesLoader extends Wire {
return $this->pages->find($selector, $options)->getTotal();
}
/**
* Preload/Prefetch fields for page together as a group (experimental)
*
* This is an optimization that enables you to load the values for multiple fields into
* a page at once, and often in a single query. This is similar to the `joinFields` option
* when loading a page, or the `autojoin` option configured with a field, except that it
* can be used after a page is already loaded. It provides a performance improvement
* relative lazy-loading of fields individually as they are accessed.
*
* Preload works only with Fieldtypes that do not override the cores loading methods.
* Preload also does not work with FieldtypeMulti types at present, except for the Page
* Fieldtype when configured to load a single page. Though it can be enabled for testing
* purposes using the `useFieldtypeMulti` $options argument.
*
* NOTE: This function is currently experimental, recommended for testing only.
*
* @param Page $page Page to preload fields for
* @param array $fieldNames Names of fields to preload
* @param array $options
* - `debug` (bool): Specify true to include additional debug info in return value (default=false).
* - `useFieldtypeMulti` (bool): Enable FieldtypeMulti for testing purposes (default=false).
* - `loadPageRefs` (bool): Optimization to early load pages in page reference fields? (default=true)
* @return array Array containing what was loaded and skipped
* @since 3.0.243
*
*/
public function preloadFields(Page $page, array $fieldNames, $options = array()) {
$defaults = [
'debug' => is_bool($options) ? $options : false,
'useFieldtypeMulti' => false,
'loadPageRefs' => true,
];
static $level = 0;
$options = is_array($options) ? array_merge($defaults, $options) : $defaults;
$debug = $options['debug'];
$database = $this->wire()->database;
$fieldNames = array_unique($fieldNames);
$fields = $page->wire()->fields;
$loadFields = [];
$loadedFields = [];
$selects = [];
$joins = [];
$numJoins = 0;
$maxJoins = 60;
$log = [
'loaded' => [],
'skipped' => [],
'blank' => [],
'queries' => 1,
];
if(!$page->id || !$page->template) return $log;
foreach($fieldNames as $fieldKey => $fieldName) {
// identify which fields to load and which to skip
$field = $fields->get($fieldName);
$fieldName = $field ? $field->name : '';
$fieldNames[$fieldKey] = $fieldName;
$error = $field ? $this->skipPreloadField($page, $field, $options) : 'Field not found';
if($error) {
unset($fieldNames[$fieldKey]);
if($fieldName) $log['skipped'][] = "$fieldName ($error)";
continue;
}
$fieldtype = $field->type;
$schema = $fieldtype->trimDatabaseSchema($fieldtype->getDatabaseSchema($field));
$numJoins += count($schema);
if($numJoins >= $maxJoins) break;
$loadFields[$fieldName] = $field;
$table = $field->getTable();
// build selects and joins
foreach(array_keys($schema) as $colName) {
if($options['useFieldtypeMulti'] && $fieldtype instanceof FieldtypeMulti) {
$sep = FieldtypeMulti::multiValueSeparator;
$orderBy = "ORDER BY $table.sort";
$selects[] = "GROUP_CONCAT($table.$colName $orderBy SEPARATOR '$sep') AS `{$table}__$colName`";
} else {
$selects[] = "$table.$colName AS {$table}__$colName";
}
$joins[$table] = "LEFT JOIN $table ON $table.pages_id=pages.id";
}
unset($fieldNames[$fieldKey]);
}
if(!count($selects)) return $log;
$trackChanges = $level ? null : $page->trackChanges();
if($trackChanges) $page->setTrackChanges(false);
$level++;
$timer = $debug ? Debug::timer() : false;
// build and execute the query
$sql =
'SELECT ' . implode(",\n", $selects) . ' ' .
"\nFROM pages " .
"\n" . implode(" \n", $joins) . ' ' .
"\nWHERE pages.id=:pid";
$query = $database->prepare($sql);
$query->bindValue(':pid', $page->id, \PDO::PARAM_INT);
$query->execute();
$data = [];
$row = $query->fetch(\PDO::FETCH_ASSOC);
$query->closeCursor();
// combine data from DB into column groups by field name
if($row) {
foreach($row as $key => $value) {
list($table, $colName) = explode('__', $key, 2);
list(, $fieldName) = explode('_', $table, 2);
if(!isset($data[$fieldName])) $data[$fieldName] = [];
$data[$fieldName][$colName] = $value;
}
}
// wake up loaded values and populate to $page
$pageIds = [];
foreach($data as $fieldName => $sleepValue) {
if(!isset($loadFields[$fieldName])) {
unset($data[$fieldName]);
continue;
}
$field = $loadFields[$fieldName];
$fieldtype = $field->type;
$cols = array_keys($sleepValue);
if(count($cols) === 1 && array_key_exists('data', $sleepValue)) {
$sleepValue = $sleepValue['data'];
}
if($sleepValue === null) {
unset($data[$fieldName]);
continue; // force to getBlankValue in loop below this
}
if($options['useFieldtypeMulti'] && $fieldtype instanceof FieldtypeMulti) {
if(strrpos($sleepValue, FieldtypeMulti::multiValueSeparator)) {
$sleepValue = explode(FieldtypeMulti::multiValueSeparator, $sleepValue);
}
}
if($fieldtype instanceof FieldtypePage && $sleepValue && $options['loadPageRefs']) {
if(!is_array($sleepValue)) $sleepValue = [ $sleepValue ];
foreach($sleepValue as $pageId) {
$pageId = (int) $pageId;
if(!$pageId) continue;
if($this->pages->cacher()->hasCache($pageId)) continue;
$parentId = $field->get('parent_id');
$templateId = FieldtypePage::getTemplateIDs($field, true);
if(!ctype_digit("$parentId")) $parentId = 0;
if(!ctype_digit("$templateId")) $templateId = 0;
$groupKey = "$parentId,$templateId";
if(!isset($pageIds[$groupKey])) $pageIds[$groupKey] = [];
$pageIds[$groupKey][$pageId] = $pageId;
}
}
$data[$fieldName] = $sleepValue;
}
// preload all pages in template or parent groups
if(count($pageIds)) {
foreach($pageIds as $groupKey => $ids) {
list($parentId, $templateId) = explode(',', $groupKey);
$this->pages->getByID($ids, [ 'template' => $templateId, 'parent_id' => $parentId ]);
}
}
foreach($data as $fieldName => $sleepValue) {
$field = $loadFields[$fieldName];
$fieldtype = $field->type;
$value = $fieldtype->wakeupValue($page, $field, $sleepValue);
$page->_parentSet($field->name, $value);
$loadedFields[$field->name] = $fieldName;
unset($loadFields[$field->name]);
$log['loaded'][] = $fieldName;
}
// any remaining loadFields not present in DB should get blank value
foreach($loadFields as $field) {
$value = $field->type->getBlankValue($page, $field);
$fieldName = $field->name;
$page->_parentSet($fieldName, $value);
$log['blank'][] = $fieldName;
}
// go recursive for any remaining fields
if(count($fieldNames)) {
$result = $this->preloadFields($page, $fieldNames, $options);
foreach($log as $key => $value) {
if(is_array($value)) {
$log[$key] = array_merge($value, $result[$key]);
} else if(is_int($value)) {
$log[$key] += $result[$key];
}
}
}
$level--;
if($debug && $timer && !$level) $log['timer'] = Debug::timer($timer);
if($trackChanges) $page->setTrackChanges($trackChanges);
return $log;
}
/**
* Preload all supported fields for given page (experimental)
*
* NOTE: This function is currently experimental, recommended for testing only.
*
* @param Page $page Page to preload fields for
* @param array $options
* - `debug` (bool): Specify true to return array of debug info (default=false).
* - `skipFieldNames` (array): Optional names of fields to skip over (default=[]).
* - See the `PagesLoader::preloadFields()` method for additional options.
* @return array Array of details
* @since 3.0.243
*
*/
public function preloadAllFields(Page $page, $options = array()) {
$fieldNames = [];
$skipFieldNames = isset($options['skipFieldNames']) ? $options['skipFieldNames'] : false;
foreach($page->template->fieldgroup as $field) {
if($skipFieldNames && in_array($field->name, $skipFieldNames)) continue;
$fieldNames[] = $field->name;
}
return $this->preloadFields($page, $fieldNames, $options);
}
/**
* Skip preloading of this field or fieldtype?
*
* Returns populated string with reason if yes, or blank string if no.
*
* @param Page $page
* @param Field $field
* @param array $options
* @return string
*
*/
protected function skipPreloadField(Page $page, Field $field, array $options) {
static $fieldtypeErrors = [];
$useFieldtypeMulti = isset($options['useFieldtypeMulti']) ? $options['useFieldtypeMulti'] : false;
$error = '';
if($page->_parentGet($field->name) !== null) {
$error = 'Already loaded';
} else if(!$page->template->fieldgroup->hasField($field)) {
$error = "Template '$page->template' does not have field";
} else if(!$field->getTable()) {
$error = 'Field has no table';
}
if($error) return $error;
$fieldtype = $field->type;
$shortName = $fieldtype->shortName;
$cacheName = $shortName;
if($fieldtype instanceof FieldtypePage) {
$cacheName .= $field->get('derefAsPage');
}
if(isset($fieldtypeErrors[$cacheName])) {
return $fieldtypeErrors[$cacheName];
}
// fieldtype status not yet known
$schema = $fieldtype->getDatabaseSchema($field);
$xtra = isset($schema['xtra']) ? $schema['xtra'] : [];
if($fieldtype instanceof FieldtypeMulti) {
if($useFieldtypeMulti) {
// allow group_concat for FieldtypeMulti
} else if($fieldtype instanceof FieldtypePage && $field->get('derefAsPage') > 0) {
// allow single-page matches
} else {
$error = "$shortName: Unsupported without useFieldtypeMulti=true";
}
} else if($fieldtype instanceof FieldtypeFieldsetOpen) {
$error = 'Fieldset: Unsupported';
}
if(!$error && isset($xtra['all']) && $xtra['all'] === false) {
if($shortName !== 'Repeater' && $shortName !== 'RepeaterMatrix') {
$error = "$shortName: External storage";
}
}
if(!$error) {
$ref = new \ReflectionClass($fieldtype);
// identify parent class that implements loadPageField method
$info = $ref->getMethod('___loadPageField');
$class = wireClassName($info->class);
// whitelist of classes with custom loadPageField methods we support
$rootClasses = [
'Fieldtype',
'FieldtypeMulti',
'FieldtypeTextarea',
'FieldtypeTextareaLanguage'
];
if(!in_array($class, $rootClasses)) {
$error = "$shortName: Has custom loader";
}
}
$fieldtypeErrors[$cacheName] = $error;
return $error;
}
/**
* Remove pages from already-loaded PageArray aren't visible or accessible
*

View File

@@ -91,6 +91,18 @@ class PagesLoaderCache extends Wire {
return $page;
}
/**
* Is given page ID in the cache?
*
* @param int page ID
* @return bool
* @since 3.0.243
*
*/
public function hasCache($id) {
return isset($this->pageIdCache[$id]);
}
/**
* Cache the given page.
*
@@ -149,13 +161,13 @@ class PagesLoaderCache extends Wire {
/**
* Remove all pages from the cache
*
* @param Page $page Optional Page that initiated the uncacheAll
* @param Page|null $page Optional Page that initiated the uncacheAll
* @param array $options Additional options to modify behavior:
* - `shallow` (bool): By default, this method also calls $page->uncache(). To prevent call to $page->uncache(), set 'shallow' => true.
* @return int Number of pages uncached
*
*/
public function uncacheAll(Page $page = null, array $options = array()) {
public function uncacheAll(?Page $page = null, array $options = array()) {
if($page) {} // to ignore unused parameter inspection
$user = $this->wire()->user;

View File

@@ -578,7 +578,12 @@ class PagesParents extends Wire {
*/
public function movePage(Page $page, Page $oldParent, Page $newParent) {
$key = "$page,$oldParent,$newParent";
if($key === $this->movePageLast) return 0;
$this->movePageLast = $key;
$database = $this->wire()->database;
$numChildren = $page->numChildren();
$numRows = 0;
$oldParentIds = $oldParent->parents()->explode('id');
@@ -595,7 +600,11 @@ class PagesParents extends Wire {
$query->bindValue(':new_parent_id', $newParent->id, \PDO::PARAM_INT);
$query->bindValue(':pages_id', $page->id, \PDO::PARAM_INT);
$query->bindValue(':old_parent_id', $oldParent->id, \PDO::PARAM_INT);
$query->execute();
try {
$query->execute();
} catch(\Exception $e) {
if($e->getCode() != 23000) throw $e;
}
$numRows += $query->rowCount();
// find children and descendents of the page that moved
@@ -604,7 +613,7 @@ class PagesParents extends Wire {
$query->bindValue(':pages_id', $page->id, \PDO::PARAM_INT);
$query->execute();
$ids = array();
$ids = array($page->id => $page->id);
while($row = $query->fetch(\PDO::FETCH_NUM)) {
$id = (int) $row[0];
$ids[$id] = $id;
@@ -612,17 +621,30 @@ class PagesParents extends Wire {
$query->closeCursor();
if(!count($ids)) return $numRows;
$inserts = array();
foreach($ids as $id) {
foreach($newParentIds as $parentId) {
$inserts["$id,$parentId"] = array('pages_id' => $id, 'parents_id' => (int) $parentId);
if($id === $parentId) continue;
$inserts[] = "$id,$parentId";
}
}
// redundancy to capture specific missing parent situations
foreach($newParent->parents() as $parent) {
if($parent->id < 2) continue;
$inserts[] = "$newParent->id,$parent->id";
if($parent->parent_id > 1) {
$grandParent = $parent->parent();
$inserts[] = "$parent->id,$grandParent->id";
}
}
if(count($oldParentIds)) {
// if page has children also add it to the inserts list
if($numChildren) $inserts[] = "$page->id,$newParent->id";
// delete old parent IDs
if(count($oldParentIds) && count($ids)) {
$idStr = implode(',', $ids);
$oldParentIds = $this->wire()->sanitizer->intArray($oldParentIds);
$oldParentIdStr = implode(',', $oldParentIds);
@@ -636,18 +658,25 @@ class PagesParents extends Wire {
$query = $database->prepare($sql);
foreach($inserts as $insert) {
$query->bindValue(':pages_id', $insert['pages_id'], \PDO::PARAM_INT);
$query->bindValue(':parents_id', $insert['parents_id'], \PDO::PARAM_INT);
list($id, $parentId) = explode(',', $insert, 2);
$query->bindValue(':pages_id', $id, \PDO::PARAM_INT);
$query->bindValue(':parents_id', $parentId, \PDO::PARAM_INT);
try {
if($query->execute()) $numRows++;
} catch(\Exception $e) {
$this->error($e->getMessage());
if($e->getCode() != 23000) $this->error($e->getMessage());
}
}
return $numRows;
}
/**
* @var string
*
*/
protected $movePageLast = '';
/**
* Add rows for a new parent in the pages_parents table
*

View File

@@ -15,7 +15,7 @@
* afterwards when appropriate.
* #pw-body
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* @todo:
@@ -131,9 +131,9 @@ class PagesPathFinder extends Wire {
protected function init($path, array $options) {
$this->options = array_merge($this->defaults, $options);
$this->verbose = $this->options['verbose'];
$this->methods = array();
$this->useLanguages = $this->options['useLanguages'] ? $this->languages(true) : array();
$this->verbose = $this->options['verbose'] || !empty($this->useLanguages);
$this->result = $this->getBlankResult(array('request' => $path));
$this->template = null;
$this->admin = null;
@@ -380,8 +380,10 @@ class PagesPathFinder extends Wire {
*
*/
protected function applyPagesRow(array $parts, $row) {
$maxUrlSegmentLength = $this->wire()->config->maxUrlSegmentLength;
$config = $this->wire()->config;
$maxUrlSegmentLength = $config->maxUrlSegmentLength;
$maxUrlSegments = $config->maxUrlSegments;
$result = &$this->result;
// array of [language name] => [ 'a', 'b', 'c' ] (from /a/b/c/)
@@ -396,14 +398,28 @@ class PagesPathFinder extends Wire {
if(!$id) {
// if it didnt resolve to DB page name then it is a URL segment
if(strlen($name) > $maxUrlSegmentLength) $name = substr($name, 0, $maxUrlSegmentLength);
$result['urlSegments'][] = $name;
if($this->verbose) {
$result['parts'][] = array(
'type' => 'urlSegment',
'value' => $name,
'language' => ''
);
if(strlen($name) > $maxUrlSegmentLength) {
$name = substr($name, 0, $maxUrlSegmentLength);
if($config->longUrlResponse >= 300) {
$result['response'] = $config->longUrlResponse;
$this->addResultError('urlSegmentLength', 'URL segment length > config.maxUrlSegmentLength');
}
}
if(count($result['urlSegments']) + 1 > $maxUrlSegments) {
if($config->longUrlResponse >= 300) {
$this->addResultError('urlSegmentMAX', 'Number of URL segments exceeds config.maxUrlSegments');
$result['response'] = $config->longUrlResponse;
break;
}
} else {
$result['urlSegments'][] = $name;
if($this->verbose) {
$result['parts'][] = array(
'type' => 'urlSegment',
'value' => $name,
'language' => ''
);
}
}
continue;
}
@@ -480,7 +496,7 @@ class PagesPathFinder extends Wire {
* If language segment detected then remove it and populate language to result
*
* @param string $path
* @return array|bool
* @return array
*
*/
protected function getPathParts($path) {
@@ -497,7 +513,7 @@ class PagesPathFinder extends Wire {
$lastPart = '';
if($this->strlen($path) > $maxPathLength) {
$result['response'] = 414; // 414=URI too long
$result['response'] = $config->longUrlResponse; // 414=URI too long
$this->addResultError('pathLengthMAX', "Path length exceeds max allowed $maxPathLength");
$path = substr($path, 0, $maxPathLength);
}
@@ -506,7 +522,7 @@ class PagesPathFinder extends Wire {
if(count($parts) > $maxDepth) {
$parts = array_slice($parts, 0, $maxDepth);
$result['response'] = 414;
$result['response'] = $config->longUrlResponse;
$this->addResultError('pathDepthMAX', 'Path depth exceeds config.maxUrlDepth');
} else if($path === '/' || $path === '' || !count($parts)) {
return array();
@@ -789,7 +805,7 @@ class PagesPathFinder extends Wire {
// if there were any non-default language segments, let that dictate the language
if(empty($result['language']['segment'])) {
$useLangName = 'default';
$useLangName = count($result['parts']) ? 'default' : $result['language']['name'];
foreach($result['parts'] as $part) {
$langName = $part['language'];
if(empty($langName) || $langName === 'default') continue;
@@ -934,7 +950,7 @@ class PagesPathFinder extends Wire {
$result['methods'] = $this->methods;
if(!$this->verbose) unset($result['parts'], $result['methods']);
if(!$this->options['verbose']) unset($result['parts'], $result['methods']);
if(empty($errors)) {
// force errors placeholder to end if there arent any
@@ -1484,7 +1500,7 @@ class PagesPathFinder extends Wire {
*
*/
protected function addResultError($name, $message, $force = false) {
if(!$this->verbose && !$force) return;
//if(!$this->verbose && !$force) return;
$this->result['errors'][$name] = $message;
}

View File

@@ -837,7 +837,7 @@ class PagesRawFinder extends Wire {
if(!isset($templatesById[$templateId])) $templatesById[$templateId] = $templates->get($templateId);
$template = $templatesById[$templateId]; /** @var Template $template */
$slash = $template->slashUrls ? '/' : '';
$path = strlen($value) && $value !== '/' ? "$value$slash" : '';
$path = strlen("$value") && $value !== '/' ? "$value$slash" : '';
if(isset($this->runtimeFields['url'])) {
$this->values[$id]['url'] = $rootUrl . $path;
}
@@ -861,11 +861,11 @@ class PagesRawFinder extends Wire {
protected function findCustom() {
if(count($this->customFields)) {
// one or more custom fields requested
if($this->ids === null) {
if($this->ids === null && !empty($this->selector)) {
// only find IDs if we didnt already in the nativeFields section
$this->setIds($this->findIDs($this->selector, false));
}
if(!count($this->ids)) return;
if(empty($this->ids)) return;
foreach($this->customFields as $fieldName => $field) {
/** @var Field $field */
$cols = isset($this->customCols[$fieldName]) ? $this->customCols[$fieldName] : array();
@@ -1144,11 +1144,12 @@ class PagesRawFinder extends Wire {
$this->wire($finder);
$options = $this->options;
$options['indexed'] = true;
$pageRefRows = $finder->find($pageRefIds, $pageRefCols, $options);
$pageRefRows = count($pageRefIds) ? $finder->find($pageRefIds, $pageRefCols, $options) : array();
foreach($this->values as $pageId => $pageRow) {
if(!isset($pageRow[$fieldName])) continue;
foreach($pageRow[$fieldName] as $pageRefId) {
if(!isset($pageRefRows[$pageRefId])) continue;
$this->values[$pageId][$fieldName][$pageRefId] = $pageRefRows[$pageRefId];
}
if(!$this->getMultiple && $field->get('derefAsPage') > 0) {

View File

@@ -403,6 +403,25 @@ class PagesRequest extends Wire {
return $page;
}
/**
* Get array of page info (as provided by PagePathFinder)
*
* See the PagesPathFinder::get() method return value for a description of
* what this method returns.
*
* If this method returns a blank array, it means that the getPage()
* method has not yet been called or that it did not match a page.
*
* #pw-advanced
*
* @return array
* @since 3.0.242
*
*/
public function getPageInfo() {
return $this->pageInfo;
}
/**
* Update/get page for given user
@@ -574,9 +593,15 @@ class PagesRequest extends Wire {
}
$maxUrlDepth = $config->maxUrlDepth;
if($maxUrlDepth > 0 && substr_count($it, '/') > $config->maxUrlDepth) {
$this->setResponseCode(414, 'Request URL exceeds max depth set in $config->maxUrlDepth');
return false;
if($maxUrlDepth > 0 && substr_count($it, '/') > $maxUrlDepth) {
if(in_array($config->longUrlResponse, [ 302, 301 ])) {
$parts = array_slice(explode('/', $it), 0, $maxUrlDepth);
$it = '/' . trim(implode('/', $parts), '/') . '/';
$this->setRedirectPath($it, $config->longUrlResponse);
} else {
$this->setResponseCode($config->longUrlResponse, 'Request URL exceeds max depth set in $config->maxUrlDepth');
return false;
}
}
if(!isset($it[0]) || $it[0] != '/') $it = "/$it";
@@ -710,7 +735,7 @@ class PagesRequest extends Wire {
* @return string|Page|null Login page object or string w/redirect URL, null if 404
*
*/
public function ___getLoginPageOrUrl(Page $page = null) {
public function ___getLoginPageOrUrl(?Page $page = null) {
$config = $this->wire()->config;

View File

@@ -5,7 +5,7 @@
*
* Implements page trash/restore/empty methods of the $pages API variable
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
*/
@@ -18,6 +18,14 @@ class PagesTrash extends Wire {
*/
protected $pages;
/**
* Last action, i.e. "restore:1234"
*
* @var int
*
*/
protected $lastAction = '';
/**
* Construct
*
@@ -41,17 +49,18 @@ class PagesTrash extends Wire {
*
*/
public function trash(Page $page, $save = true) {
if(!$this->pages->isDeleteable($page) || $page->template->noTrash) {
throw new WireException("This page (id=$page->id) may not be placed in the trash");
}
$trash = $this->pages->get($this->config->trashPageID);
$trash = $this->pages->get($this->wire()->config->trashPageID);
if(!$trash->id) {
throw new WireException("Unable to load trash page defined by config::trashPageID");
}
$this->pages->trashReady($page);
if($this->lastAction != "trash:$page") $this->pages->trashReady($page);
$page->addStatus(Page::statusTrash);
@@ -70,10 +79,10 @@ class PagesTrash extends Wire {
// make the name unique when in trash, to avoid namespace collision and maintain parent restore info
$name = $page->id;
if($parentPrevious && $parentPrevious->id) {
$name .= "." . $parentPrevious->id;
$name .= "." . $page->sort;
$sort = $page->get('sortPrevious|sort');
$name .= ".$parentPrevious->id.$sort";
}
$page->name = ($name . "_" . $page->name);
$page->name = ($name . '_' . $page->name);
// do the same for other languages, if present
$languages = $this->wire()->languages;
@@ -87,9 +96,13 @@ class PagesTrash extends Wire {
}
}
if($save) $this->pages->save($page);
$this->lastAction = "trash:$page";
if($save) {
$this->pages->save($page, array('caller' => 'pages.trash', 'callback' => 'trashed'));
}
$this->pages->editor()->savePageStatus($page->id, Page::statusTrash, true, false);
if($save) $this->pages->trashed($page);
$this->pages->debugLog('trash', $page, true);
return true;
@@ -107,24 +120,31 @@ class PagesTrash extends Wire {
*
*/
public function restore(Page $page, $save = true) {
$info = $this->getRestoreInfo($page, true);
if($info['restorable']) {
// we detected original parent
if($save) $page->save();
if($this->lastAction !== "restore:$page") $this->pages->restoreReady($page);
$this->pages->editor()->savePageStatus($page->id, Page::statusTrash, true, true);
} else if(!$page->parent->isTrash()) {
// page has had new parent already set
if($this->lastAction !== "restore:$page") $this->pages->restoreReady($page);
$page->removeStatus(Page::statusTrash);
if($save) $page->save();
$this->pages->editor()->savePageStatus($page->id, Page::statusTrash, true, true);
if($save) $this->pages->restored($page);
$this->pages->debugLog('restore', $page, true);
} else {
// page is in trash and we cannot detect new parent
return false;
}
$this->lastAction = "restore:$page";
if($save) {
$this->pages->save($page, array('caller' => 'pages.restore', 'callback' => 'restored'));
}
return true;
}
@@ -221,6 +241,7 @@ class PagesTrash extends Wire {
if($populateToPage) {
$page->name = $name;
$page->removeStatus(Page::statusTrash);
if($newParent) {
$page->sort = $sort;
$page->parent = $newParent;

View File

@@ -17,6 +17,7 @@
* https://processwire.com
*
* @method Page add($name)
* @method Page new(array $options = []) 3.0.249
* @method bool save(Page $page)
* @method bool delete(Page $page, $recursive = false)
*
@@ -89,6 +90,23 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
parent::__construct();
}
/**
* Create new instance of this page type
*
* @param array $options
* @return Page
* @since 3.0.249
*
*/
public function ___new(array $options = []) {
$defaults = array(
'template' => $this->getTemplate(),
'parent' => $this->getParent(),
'pageClass' => $this->getPageClass()
);
return $this->wire()->pages->newPage(array_merge($defaults, $options));
}
/**
* Add one or more templates that this PagesType represents
*

View File

@@ -54,7 +54,7 @@
*
* #pw-body
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* This file is licensed under the MIT license
@@ -108,6 +108,14 @@ class Paths extends WireData {
*/
protected $_root = '';
/**
* As used by get() method
*
* @var null
*
*/
protected $_http = null;
/**
* Construct the Paths
*
@@ -117,6 +125,7 @@ class Paths extends WireData {
public function __construct($root) {
$this->_root = $root;
$this->useFuel(false);
parent::__construct();
}
/**
@@ -165,20 +174,19 @@ class Paths extends WireData {
*
*/
public function get($key) {
static $_http = null;
if($key === 'root') return $this->_root;
$http = '';
$altKey = '';
if(is_object($key)) {
$key = "$key";
} else if(strpos($key, 'http') === 0) {
if(is_null($_http)) {
if($this->_http === null) {
$scheme = $this->wire()->input->scheme;
if(!$scheme) $scheme = 'http';
$httpHost = $this->wire()->config->httpHost;
if($httpHost) $_http = "$scheme://$httpHost";
if($httpHost) $this->_http = "$scheme://$httpHost";
}
$http = $_http;
$http = $this->_http;
$key = substr($key, 4); // httpTemplates => Templates
$altKey = $key; // no lowercase conversion (useful for keys like module names, i.e. 'ProcessPageEdit')
$key[0] = strtolower($key[0]); // first character lowercase: Templates => templates

View File

@@ -47,10 +47,10 @@ class Permission extends Page {
/**
* Create a new Permission page in memory.
*
* @param Template $tpl Template object this page should use.
* @param Template|null $tpl Template object this page should use.
*
*/
public function __construct(Template $tpl = null) {
public function __construct(?Template $tpl = null) {
parent::__construct($tpl);
if(!$tpl) $this->template = $this->wire()->templates->get('permission');
$this->_parent_id = $this->wire()->config->permissionsPageID;
@@ -145,5 +145,3 @@ class Permission extends Page {
return $this->wire()->permissions;
}
}

View File

@@ -16,6 +16,7 @@
* @method void added(Page $page) Hook called just after a Permission is added #pw-hooker
* @method void deleteReady(Page $page) Hook called before a Permission is deleted #pw-hooker
* @method void deleted(Page $page) Hook called after a permission is deleted #pw-hooker
* @method Permission new($options = []) Create new Permission instance in memory (3.0.249+)
*
*/
class Permissions extends PagesType {

View File

@@ -251,6 +251,8 @@ abstract class Process extends WireData implements Module {
*
*/
public function ___breadcrumb($href, $label) {
if(is_array($label)) return $this;
$label = (string) $label;
$pos = strpos($label, '/');
if($pos !== false && strpos($href, '/') === false) {
// arguments got reversed, we'll work with it anyway...

View File

@@ -328,7 +328,8 @@ class ProcessController extends Wire {
public function ___execute() {
$debug = $this->wire()->config->debug;
$breadcrumbs = $this->wire()->breadcrumbs;
$breadcrumbs = $this->wire()->breadcrumbs;
$adminTheme = $this->wire()->adminTheme;
$headline = $this->wire('processHeadline');
$numBreadcrumbs = $breadcrumbs ? count($breadcrumbs) : null;
$process = $this->getProcess();
@@ -398,6 +399,20 @@ class ProcessController extends Wire {
$content = '';
}
}
if(!$process instanceof WirePageEditor) {
$headline = (string) $this->wire('processHeadline');
if(strlen($headline)) {
if(strpos($headline, '<icon-') === false) {
// $icon = $this->wire()->modules->getModuleInfoProperty('icon');
// if($icon) $process->headline("<icon-$icon> $headline");
} else {
if(!$adminTheme instanceof AdminThemeFramework) {
$process->headline(preg_replace('/(?:<|&lt;)icon-[-a-z0-9]+(?:>|&gt;)/', '', $headline));
}
}
}
}
return $content;
}
@@ -486,18 +501,22 @@ class ProcessController extends Wire {
/**
* Generate a message in JSON format, for use with AJAX output
*
* @param string $msg
* @param bool $error
* @param bool $allowMarkup
* @param string|array $msg Message string or in 3.0.246+ also accepts an array of extra data
* When using an array, please include a 'message' index with text about the error or non-error.
* @param bool $error Is this in error message? Default is true, or specify false if not.
* @param bool $allowMarkup Allow markup in message? Applies only to $msg string or 'message' index of array (default=false)
* @return string JSON encoded string
*
*/
public function jsonMessage($msg, $error = false, $allowMarkup = false) {
if(!$allowMarkup) $msg = $this->wire()->sanitizer->entities($msg);
return json_encode(array(
'error' => (bool) $error,
'message' => (string) $msg
));
$a = array('error' => (bool) $error, 'message' => '');
if(is_array($msg)) {
$a = array_merge($a, $msg);
} else {
$a['message'] = (string) $msg;
}
if(!$allowMarkup) $a['message'] = $this->wire()->sanitizer->entities($a['message']);
return json_encode($a);
}
/**

View File

@@ -17,7 +17,7 @@ require_once(__DIR__ . '/boot.php');
* ~~~~~
* #pw-body
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
* https://processwire.com
*
* Default API vars (A-Z)
@@ -54,6 +54,7 @@ require_once(__DIR__ . '/boot.php');
* @property Users $users
* @property ProcessWire $wire
* @property WireShutdown $shutdown
* @property PagesVersions|null $pagesVersions
*
* @method init()
* @method ready()
@@ -79,7 +80,7 @@ class ProcessWire extends Wire {
* Reversion revision number
*
*/
const versionRevision = 227;
const versionRevision = 251;
/**
* Version suffix string (when applicable)
@@ -287,7 +288,7 @@ class ProcessWire extends Wire {
// this is reset in the $this->setConfig() method based on current debug mode
ini_set('display_errors', true);
error_reporting(E_ALL | E_STRICT);
error_reporting(E_ALL);
$config->setWire($this);
@@ -448,7 +449,7 @@ class ProcessWire extends Wire {
if($debug) {
// If debug mode is on then echo all errors
error_reporting(E_ALL | E_STRICT);
error_reporting(E_ALL);
ini_set('display_errors', 1);
} else {
// disable all error reporting
@@ -600,7 +601,9 @@ class ProcessWire extends Wire {
// the current user can only be determined after the session has been initiated
$session = $this->wire('session', new Session($this), true);
$this->initVar('session', $session);
$this->wire('user', $users->getCurrentUser());
$user = $users->getCurrentUser();
if($config->userOutputFormatting) $user->of(true);
$this->wire('user', $user);
$input = $this->wire('input', new WireInput(), true);
if($config->wireInputLazy) $input->setLazy(true);

View File

@@ -71,6 +71,14 @@ class Punycode {
*/
protected $encoding;
/**
* PHP mb string functions supported?
*
* @var bool
*
*/
protected $mb = false;
/**
* Constructor
*
@@ -78,7 +86,12 @@ class Punycode {
*/
public function __construct($encoding = 'UTF-8') {
$this->encoding = $encoding;
$this->mb = function_exists("mb_internal_encoding");
}
public function strtolower($str) { return $this->mb ? mb_strtolower($str, $this->encoding) : strtolower($str); }
public function strlen($str) { return $this->mb ? mb_strlen($str, $this->encoding) : strlen($str); }
public function substr($str, $a, $b) { return $this->mb ? mb_substr($str, $a, $b, $this->encoding) : substr($str, $a, $b); }
/**
* Encode a domain to its Punycode version
@@ -88,7 +101,7 @@ class Punycode {
* @return string Punycode representation in ASCII
*/
public function encode($input) {
$input = mb_strtolower($input, $this->encoding);
$input = $this->strtolower($input);
$parts = explode('.', $input);
foreach($parts as &$part) {
$part = $this->encodePart($part);
@@ -122,7 +135,7 @@ class Punycode {
$codePoints['nonBasic'] = array_unique($codePoints['nonBasic']);
sort($codePoints['nonBasic']);
$i = 0;
$length = mb_strlen($input, $this->encoding);
$length = $this->strlen($input);
while($h < $length) {
$m = $codePoints['nonBasic'][$i++];
$delta = $delta + ($m - $n) * ($h + 1);
@@ -138,11 +151,11 @@ class Punycode {
if($q < $t) {
break;
}
$code = $t + (($q - $t) % (static::BASE - $t));
$code = $t + ((floor($q) - $t) % (static::BASE - $t));
$output .= static::$encodeTable[$code];
$q = ($q - $t) / (static::BASE - $t);
}
$output .= static::$encodeTable[$q];
$output .= static::$encodeTable[floor($q)];
$bias = $this->adapt($delta, $h + 1, ($h === $b));
$delta = 0;
$h++;
@@ -209,9 +222,9 @@ class Punycode {
$bias = $this->adapt($i - $oldi, ++$outputLength, ($oldi === 0));
$n = $n + (int) ($i / $outputLength);
$i = $i % ($outputLength);
$output = mb_substr($output, 0, $i, $this->encoding) .
$output = $this->substr($output, 0, $i) .
$this->codePointToChar($n) .
mb_substr($output, $i, $outputLength - 1, $this->encoding);
$this->substr($output, $i, $outputLength - 1);
$i++;
}
return $output;
@@ -272,9 +285,9 @@ class Punycode {
'basic' => array(),
'nonBasic' => array(),
);
$length = mb_strlen($input, $this->encoding);
$length = $this->strlen($input);
for($i = 0; $i < $length; $i++) {
$char = mb_substr($input, $i, 1, $this->encoding);
$char = $this->substr($input, $i, 1);
$code = $this->charToCodePoint($char);
if($code < 128) {
$codePoints['all'][] = $codePoints['basic'][] = $code;
@@ -323,4 +336,4 @@ class Punycode {
return chr(($code >> 18) + 240) . chr((($code >> 12) & 63) + 128) . chr((($code >> 6) & 63) + 128) . chr(($code & 63) + 128);
}
}
}
}

View File

@@ -25,10 +25,10 @@ class Role extends Page {
/**
* Create a new Role page in memory.
*
* @param Template $tpl
* @param Template|null $tpl
*
*/
public function __construct(Template $tpl = null) {
public function __construct(?Template $tpl = null) {
parent::__construct($tpl);
}

View File

@@ -14,6 +14,7 @@
* @method void added(Page $page) Hook called just after a Role is added #pw-hooker
* @method void deleteReady(Page $page) Hook called before a Role is deleted #pw-hooker
* @method void deleted(Page $page) Hook called after a Role is deleted #pw-hooker
* @method Role new($options = []) Create new Role instance in memory (3.0.249+)
*
*/

View File

@@ -90,7 +90,7 @@
*
* #pw-body
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* @link https://processwire.com/api/variables/sanitizer/ Offical $sanitizer API variable Documentation
@@ -238,6 +238,17 @@ class Sanitizer extends Wire {
'&zwj;', // zero width join
);
/**
* Characters blacklisted from UTF-8 page names
*
* @var string[]
*
*/
protected $pageNameBlacklist = array(
'/', '\\', '%', '"', "'", '<', '>', '?', '!', '#', '@', ':', ';', ',',
'+', '=', '*', '^', '$', '(', ')', '[', ']', '{', '}', '|', '&',
);
/**
* Sanitizer method names (A-Z) and type(s) they return
*
@@ -366,8 +377,6 @@ class Sanitizer extends Wire {
*/
public function nameFilter($value, array $allowedExtras, $replacementChar, $beautify = false, $maxLength = 128) {
static $replacements = array();
if(!is_string($value)) $value = $this->string($value);
$allowed = array_merge($this->allowedASCII, $allowedExtras);
$needsWork = strlen(str_replace($allowed, '', $value));
@@ -376,27 +385,30 @@ class Sanitizer extends Wire {
if($beautify && $needsWork) {
if($beautify === self::translate && $this->multibyteSupport) {
$value = mb_strtolower($value);
$replacements = array();
if(empty($replacements)) {
if(empty($this->caches['nameFilterReplace'])) {
$modules = $this->wire()->modules;
if($modules) {
$configData = $this->wire()->modules->getModuleConfigData('InputfieldPageName');
$replacements = empty($configData['replacements']) ? InputfieldPageName::$defaultReplacements : $configData['replacements'];
$replacements = $this->wire()->modules->getConfig('InputfieldPageName', 'replacements');
if(empty($replacements)) $replacements = InputfieldPageName::$defaultReplacements;
$this->caches['nameFilterReplace'] = $replacements;
}
} else {
$replacements = $this->caches['nameFilterReplace'];
}
foreach($replacements as $from => $to) {
if(mb_strpos($value, $from) !== false) {
$value = mb_eregi_replace($from, $to, $value);
}
if(count($replacements)) {
$value = str_replace(array_keys($replacements), array_values($replacements), $value);
$needsWork = strlen(str_replace($allowed, '', $value));
}
}
if(function_exists("\\iconv")) {
if($needsWork && function_exists("\\iconv")) {
$v = iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", $value);
if($v) $value = $v;
$needsWork = strlen(str_replace($allowed, '', $value));
}
$needsWork = strlen(str_replace($allowed, '', $value));
}
if(strlen($value) > $maxLength) $value = substr($value, 0, $maxLength);
@@ -776,7 +788,8 @@ class Sanitizer extends Wire {
* - `Sanitizer::okUTF8` (constant): Allow UTF-8 characters to appear in path (implied if $config->pageNameCharset is 'UTF8').
* @param int|array $maxLength Maximum number of characters allowed in the name.
* You may also specify the $options array for this argument instead.
* @param array $options Array of options to modify default behavior. See Sanitizer::name() method for available options.
* @param array $options Array of options to modify default behavior. See Sanitizer::name() method for available options, plus:
* - `punycodeVersion` (int): Punycode version to use with UTF-8 page names, see Sanitizer::getPunycodeVersion() method for details.
* @return string
* @see Sanitizer::name()
*
@@ -787,7 +800,8 @@ class Sanitizer extends Wire {
if(!strlen($value)) return '';
$defaults = array(
'charset' => $this->wire()->config->pageNameCharset
'charset' => $this->wire()->config->pageNameCharset,
'punycodeVersion' => 0,
);
if(is_array($beautify)) {
@@ -819,19 +833,26 @@ class Sanitizer extends Wire {
&& !ctype_alnum(str_replace(array('-', '_', '.'), '', $value))
&& strpos($value, 'xn-') !== 0) {
$tt = $this->getTextTools();
$max = $maxLength;
do {
// encode value
$value = $this->punyEncodeName($_value);
$value = $this->punyEncodeName($_value, $options['punycodeVersion']);
// if result stayed within our allowed character limit, then good, we're done
if(strlen($value) <= $maxLength) break;
// continue loop until encoded value is equal or less than allowed max length
$_value = substr($_value, 0, strlen($_value) - 1);
$_value = $tt->substr($_value, 0, $max--);
} while(true);
// if encode was necessary and successful, return with no further processing
if(strpos($value, 'xn-') === 0) {
return $value;
} else {
if(strlen($value) && ctype_alnum(str_replace(array('-', '_', '.'), '', $value))) {
if($this->getPunycodeVersion($options['punycodeVersion']) > 1) return $value;
}
// can't be encoded, send to regular name sanitizer
$value = $_value;
}
@@ -842,7 +863,7 @@ class Sanitizer extends Wire {
$beautify = self::okUTF8;
if(strpos($value, 'xn-') === 0) {
// found something to convert
$value = $this->punyDecodeName($value);
$value = $this->punyDecodeName($value, $options['punycodeVersion']);
// now it will run through okUTF8
}
}
@@ -893,6 +914,7 @@ class Sanitizer extends Wire {
if(!strlen($value)) return '';
$config = $this->wire()->config;
$keepGoing = true;
// if UTF8 module is not enabled then delegate this call to regular pageName sanitizer
if($config->pageNameCharset != 'UTF8') return $this->pageName($value, false, $maxLength);
@@ -908,7 +930,8 @@ class Sanitizer extends Wire {
// whitelist of allowed characters and blacklist of disallowed characters
$whitelist = $config->pageNameWhitelist;
if(!strlen($whitelist)) $whitelist = false;
$blacklist = '/\\%"\'<>?#@:;,+=*^$()[]{}|&';
$value = str_replace($this->pageNameBlacklist, '-', $value);
// we let regular pageName handle chars like these, if they appear without other UTF-8
$extras = array('.', '-', '_', ',', ';', ':', '(', ')', '!', '?', '&', '%', '$', '#', '@');
@@ -923,43 +946,48 @@ class Sanitizer extends Wire {
if($this->caches[$k] || $tt->strtolower($value) === $value) {
// whitelist supports only lowercase OR value is all lowercase
// let regular pageName sanitizer handle this
return $this->pageName($value, false, $maxLength);
$value = $this->pageName($value, false, $maxLength);
// maintain old behavior for existing installations
if($this->getPunycodeVersion() < 2) return $value;
$keepGoing = false;
}
}
// validate that all characters are in our whitelist
$replacements = array();
if($keepGoing) {
// validate that all characters are in our whitelist
$replacements = array();
for($n = 0; $n < $tt->strlen($value); $n++) {
$c = $tt->substr($value, $n, 1);
$inBlacklist = $tt->strpos($blacklist, $c) !== false || strpos($blacklist, $c) !== false;
$inWhitelist = !$inBlacklist && $whitelist !== false && $tt->strpos($whitelist, $c) !== false;
if($inWhitelist && !$inBlacklist) {
// in whitelist
} else if($inBlacklist || !strlen(trim($c)) || ctype_cntrl($c)) {
// character does not resolve to something visible or is in blacklist
$replacements[] = $c;
} else if($whitelist === false) {
// whitelist disabled: allow everything that is not blacklisted
} else {
// character that is not in whitelist, double check case variants
$cLower = $tt->strtolower($c);
$cUpper = $tt->strtoupper($c);
if($cLower !== $c && $tt->strpos($whitelist, $cLower) !== false) {
// allow character and convert to lowercase variant
$value = $tt->substr($value, 0, $n) . $cLower . $tt->substr($value, $n+1);
} else if($cUpper !== $c && $tt->strpos($whitelist, $cUpper) !== false) {
// allow character and convert to uppercase varient
$value = $tt->substr($value, 0, $n) . $cUpper . $tt->substr($value, $n+1);
} else {
// queue character to be replaced
for($n = 0; $n < $tt->strlen($value); $n++) {
$c = $tt->substr($value, $n, 1);
if($c === '-') continue;
$inWhitelist = $whitelist !== false && $tt->strpos($whitelist, $c) !== false;
if($inWhitelist) {
// in whitelist
} else if(!strlen(trim($c)) || ctype_cntrl($c)) {
// character does not resolve to something visible
$replacements[] = $c;
} else if($whitelist === false) {
// whitelist disabled: allow everything that is not blacklisted
} else {
// character that is not in whitelist, double check case variants
$cLower = $tt->strtolower($c);
$cUpper = $tt->strtoupper($c);
if($cLower !== $c && $tt->strpos($whitelist, $cLower) !== false) {
// allow character and convert to lowercase variant
$value = $tt->substr($value, 0, $n) . $cLower . $tt->substr($value, $n + 1);
} else if($cUpper !== $c && $tt->strpos($whitelist, $cUpper) !== false) {
// allow character and convert to uppercase variant
$value = $tt->substr($value, 0, $n) . $cUpper . $tt->substr($value, $n + 1);
} else {
// queue character to be replaced
$replacements[] = $c;
}
}
}
}
// replace disallowed characters with "-"
if(count($replacements)) $value = str_replace($replacements, '-', $value);
// replace disallowed characters with "-"
if(count($replacements)) $value = str_replace($replacements, '-', $value);
}
// replace doubled word separators
foreach($separators as $c) {
@@ -980,36 +1008,51 @@ class Sanitizer extends Wire {
* Decode a PW-punycode'd name value
*
* @param string $value
* @param int $version 0=auto-detect, 1=original/buggy, 2=punycode library, 3=php idn function
* @return string
*
*/
protected function punyDecodeName($value) {
protected function punyDecodeName($value, $version = 0) {
// exclude values that we know can't be converted
if(strlen($value) < 4 || strpos($value, 'xn-') !== 0) return $value;
$version = $this->getPunycodeVersion($version);
if(strpos($value, '__')) {
// as used by punycode version 1 to split long strings
$_value = $value;
$parts = explode('__', $_value);
foreach($parts as $n => $part) {
$parts[$n] = $this->punyDecodeName($part);
$parts[$n] = $this->punyDecodeName($part, $version);
}
$value = implode('', $parts);
return $value;
}
$_value = $value;
// convert "xn-" single hyphen to recognized punycode "xn--" double hyphen
if(strpos($value, 'xn--') !== 0) $value = 'xn--' . substr($value, 3);
if(function_exists('idn_to_utf8')) {
// use native php function if available
$value = @idn_to_utf8($value);
} else {
// otherwise use Punycode class
if($version >= 3) {
// PHP IDN function
// 32=IDNA_NONTRANSITIONAL_TO_UNICODE
$info = array();
$value = idn_to_utf8($value, 32, INTL_IDNA_VARIANT_UTS46, $info);
if(empty($value)) $value = $info['result'];
} else if($version === 2) {
// Punycode library
$pc = new Punycode();
$value = $pc->decode($value);
} else {
// PHP IDN with old/buggy behavior post PHP 7.4
$value = @idn_to_utf8($value);
}
// if utf8 conversion failed, restore original value
if($value === false || !strlen($value)) $value = $_value;
return $value;
}
@@ -1017,41 +1060,92 @@ class Sanitizer extends Wire {
* Encode a name value to PW-punycode
*
* @param string $value
* @param int $version 0=auto-detect, 1=original/buggy, 2=punycode library, 3=php idn function
* @return string
*
*/
protected function punyEncodeName($value) {
// exclude values that don't need to be converted
if(strpos($value, 'xn-') === 0) return $value;
if(ctype_alnum(str_replace(array('.', '-', '_'), '', $value))) return $value;
protected function punyEncodeName($value, $version = 0) {
$tt = $this->getTextTools();
$version = $this->getPunycodeVersion($version);
if(strpos($value, 'xn-') === 0) {
if(ctype_alnum(str_replace(array('.', '-', '_'), '', $value))) {
return $value;
}
}
if($version > 1) {
$whitelist = $this->wire()->config->pageNameWhitelist;
$value = str_replace($this->pageNameBlacklist, '-', $value);
$v = '';
for($n = 0; $n < $tt->strlen($value); $n++) {
$c = $tt->substr($value, $n, 1);
if($tt->stripos($whitelist, $c) === false) {
$c = $this->pageName($c, self::translate);
if(empty($c) || $tt->stripos($whitelist, $c) === false) {
$c = '-';
}
}
$v .= $c;
}
while(strpos($v, '--') !== false) $v = str_replace('--', '-', $v);
$value = $tt->trim($v, '-');
}
if(ctype_alnum(str_replace(array('.', '-', '_'), '', $value))) {
$value = $this->pageName(trim($value), true);
return $value;
}
while(strpos($value, '__') !== false) {
$value = str_replace('__', '_', $value);
}
if(strlen($value) >= 50) {
if($version > 1) {
// version 2, 3
while(strpos($value, '--') !== false) {
$value = str_replace('--', '-', $value);
}
$value = trim($value, '-');
} else if(strlen($value) >= 50) {
// version 1
$_value = $value;
$parts = array();
while(strlen($_value)) {
$part = $tt->substr($_value, 0, 12);
$_value = $tt->substr($_value, 12);
$parts[] = $this->punyEncodeName($part);
$parts[] = $this->punyEncodeName($part, $version);
}
$value = implode('__', $parts);
return $value;
return $value;
}
$_value = $value;
if(function_exists("idn_to_ascii")) {
// use native php function if available
$value = substr(@idn_to_ascii($value), 3);
} else {
// otherwise use Punycode class
if($version >= 3) {
// PHP 7.4+ idn_to_ascii
$info = array();
// 16=IDNA_NONTRANSITIONAL_TO_ASCII
idn_to_ascii($value, 16, INTL_IDNA_VARIANT_UTS46, $info);
// IDN return value fails on longer strings, but populates result correctly
$value = $info['result'];
} else if($version === 2) {
// Punycode library
$pc = new Punycode();
$value = substr($pc->encode($value), 3);
$value = $pc->encode($value);
} else {
// buggy behavior in PHP 7.4+ but pages may already be present with it
// INTL_IDNA_VARIANT_2003 is default prior to PHP 7.4
// substr() is also not right here but kept for v1 compatibility
$value = substr(@idn_to_ascii($value), 3);
}
if(strpos($value, 'xn-') === 0) $value = substr($value, 3);
if(strlen($value) && $value !== '-') {
// in PW the xn- prefix has one fewer hyphen than in native Punycode
// for compatibility with pageName sanitization and beautification
@@ -1061,8 +1155,45 @@ class Sanitizer extends Wire {
// return value is always ascii
$value = $this->name($_value);
}
return $value;
}
/**
* Get internal Punycode version to use
*
* 0: Auto-detect from current environment.
* 1: PHP IDN function used by all PW versions prior to 3.0.244, but buggy PHP 7.4+.
* 2: Dedicated Punycode PHP library (no known issues at present).
* 3: PHP IDN function call updated for PHP 7.4+ (default in new installations after January 2025).
*
* @param int $version
* @return int 1=PHP DN but buggy after PHP 7.4+, 2=Punycode library, 3=PHP IDN function PHP 7.4+
* @since 3.0.244
*
*/
protected function getPunycodeVersion($version = 0) {
$config = $this->wire()->config;
if(!$version) {
$whitelist = $config->pageNameWhitelist;
for($n = 3; $n > 0; $n--) {
if(strpos($whitelist, "v$n") !== false) $version = $n;
if($version) break;
}
}
if(!$version) $version = $config->installedAfter('2025-01-10') ? 3 : 1;
if(!function_exists('idn_to_utf8')) $version = 2;
if($version >= 3 && version_compare(phpversion(), '7.4.0', '<')) $version = 2;
return $version;
}
/**
* @return Punycode
*
*/
protected function punycode() {
return new Punycode();
}
/**
* Format required by ProcessWire user names
@@ -1082,7 +1213,17 @@ class Sanitizer extends Wire {
* Name filter for ProcessWire filenames (basenames only, not paths)
*
* This sanitizes a filename to be consistent with the name format in ProcessWire,
* ASCII-alphanumeric, hyphens, underscores and periods.
* ASCII-alphanumeric (a-z A-Z 0-9), hyphens, underscores and periods. Note that
* filenames may contain mixed case (a-z A-Z) so if you require lowercase then
* run the return value through a `strtolower()` function.
*
* ~~~~~
* // outputs: FileName.jpg
* echo $sanitizer->filename('©®™FileName.jpg');
*
* // outputs: c_r_tmfilename.jpg
* echo strtolower($sanitizer->filename('©®™filename.jpg', Sanitizer::translate));
* ~~~~~
*
* #pw-group-strings
* #pw-group-files
@@ -1102,9 +1243,10 @@ class Sanitizer extends Wire {
if(strlen($value) > $maxLength) {
// truncate, while keeping extension in tact
$tt = $this->getTextTools();
$pathinfo = pathinfo($value);
$extLen = strlen($pathinfo['extension']) + 1; // +1 includes period
$basename = substr($pathinfo['filename'], 0, $maxLength - $extLen);
$extLen = $tt->strlen($pathinfo['extension']) + 1; // +1 includes period
$basename = $tt->substr($pathinfo['filename'], 0, $maxLength - $extLen);
$value = "$basename.$pathinfo[extension]";
}
@@ -1983,7 +2125,14 @@ class Sanitizer extends Wire {
$value = preg_replace('!</li>\s*<li!is', "$options[separator]<li", $value);
}
}
// replace single less than sign that's not accompanied with a greater than sign
// to something that looks like it, but that strip_tags() wont strip.
// this is to prevent something like "5<10" from getting converted to "5"
if(strpos($value, '<') !== false && strpos($value, '>') === false) {
$value = preg_replace('/<([\w\d])/', '≺$1', $value);
}
// remove tags
$value = trim(strip_tags($value));
@@ -2363,7 +2512,7 @@ class Sanitizer extends Wire {
} else {
// domain contains utf8
$pc = function_exists("idn_to_ascii") ? false : new Punycode();
$pc = function_exists("idn_to_ascii") ? false : $this->punycode();
$domain = $pc ? $pc->encode($domain) : @idn_to_ascii($domain);
if($domain === false || !strlen($domain)) return '';
$url = $scheme . $domain . $rest;
@@ -4431,6 +4580,7 @@ class Sanitizer extends Wire {
* - `delimiter` (string): Single delimiter to use to identify CSV strings. Overrides the 'delimiters' option when specified (default=null)
* - `delimiters` (array): Delimiters to identify CSV strings. First found delimiter will be used, default=array("|", ",")
* - `enclosure` (string): Enclosure to use for CSV strings (default=double quote, i.e. `"`)
* - `escape` (string): Escape to use for CSV strings (default=backslash, i.e. "\\")
* @return array
* @throws WireException if an unknown $sanitizer method is given
*
@@ -4446,6 +4596,7 @@ class Sanitizer extends Wire {
'delimiter' => null,
'delimiters' => array('|', ','),
'enclosure' => '"',
'escape' => "\\",
'trim' => true,
'sanitizer' => null,
'keySanitizer' => null,
@@ -4484,7 +4635,7 @@ class Sanitizer extends Wire {
}
}
if($hasDelimiter !== null) {
$value = str_getcsv($value, $hasDelimiter, $options['enclosure']);
$value = str_getcsv($value, $hasDelimiter, $options['enclosure'], $options['escape']);
} else {
$value = array($value);
}
@@ -4862,6 +5013,7 @@ class Sanitizer extends Wire {
* - `maxWordLength` (int): Maximum word length (default=80)
* - `maxWords` (int): Maximum number of words allowed (default=0, no limit)
* - `stripTags` (bool): Strip markup tags so they dont contribute to returned word list? (default=true)
* - `truncate` (bool): Truncate rather than remove words that exceed maxWordLength? (default=false) 3.0.250+
* @return array
* @since 3.0.160
*
@@ -4879,6 +5031,7 @@ class Sanitizer extends Wire {
'keepNumberFormat' => true,
'keepChars' => array(),
'stripTags' => true,
'truncate' => false,
);
$options = array_merge($defaults, $options);
@@ -4971,8 +5124,17 @@ class Sanitizer extends Wire {
continue;
}
$length = $this->multibyteSupport ? mb_strlen($word) : strlen($word);
if($length < $minLength || $length > $maxLength) {
// remove any words that are outside the min/max length requirements
if($length > $maxLength) {
// remove or truncate any words that are too long
if($options['truncate']) {
$word = $this->multibyteSupport ? mb_substr($word, 0, $maxLength) : substr($word, 0, $maxLength);
$words[$key] = $word;
} else {
unset($words[$key]);
continue;
}
} else if($length < $minLength) {
// remove any words that are are not long enough
unset($words[$key]);
continue;
} else if($keepChars !== '' && !strlen(trim($word, $keepChars))) {
@@ -5177,7 +5339,7 @@ class Sanitizer extends Wire {
* @param string|int|array|float $value
* @param int $maxLength Maximum length (default=128)
* @param null|int $maxBytes Maximum allowed bytes (used for string types only)
* @return array|bool|float|int|string
* @return array|float|int|string
* @since 3.0.125
* @see Sanitizer::minLength()
*

View File

@@ -109,6 +109,8 @@ class Selectors extends WireArray {
*/
public function __construct($selector = null) {
parent::__construct();
$this->usesNumericKeys = false;
$this->indexedByName = false;
if(!is_null($selector)) $this->init($selector);
}
@@ -771,38 +773,99 @@ class Selectors extends WireArray {
* @param Wire $item
* @return bool
*
*/
*/
public function matches(Wire $item) {
// if item provides it's own matches function, then let it have control
// if item provides it's own matches function (like Page), then let it have control
if($item instanceof WireMatchable) return $item->matches($this);
$orGroups = array();
$matches = true;
foreach($this as $selector) {
$value = array();
foreach($selector->fields as $property) {
if(strpos($property, '.') && $item instanceof WireData) {
$value[] = $item->getDot($property);
} else {
$value[] = (string) $item->$property;
}
}
if(!$selector->matches($value)) {
$matches = false;
// attempt any alternate operators, if present
foreach($selector->altOperators as $altOperator) {
$altSelector = self::getSelectorByOperator($altOperator);
if(!$altSelector) continue;
$this->wire($altSelector);
$selector->copyTo($altSelector);
$matches = $altSelector->matches($value);
if($matches) break;
}
// if neither selector nor altSelectors match then stop
if($selector->quote === '(' && self::stringHasOperator($selector->value())) {
$name = $selector->field();
if(!isset($orGroups[$name])) $orGroups[$name] = array();
$orGroups[$name][] = $selector->value;
} else {
$matches = $this->matchesSelector($selector, $item);
if(!$matches) break;
}
}
if($matches && count($orGroups)) {
$matches = $this->matchesOrGroups($orGroups, $item);
}
return $matches;
}
/**
* Does the given Wire match these Selector (single)?
*
* @param Selector $selector
* @param Wire $item
* @return bool
* @since 3.0.330
*
*/
protected function matchesSelector(Selector $selector, Wire $item) {
$value = array();
foreach($selector->fields as $property) {
if(strpos($property, '.') && $item instanceof WireData) {
$v = $item->getDot($property);
} else {
$v = $item->$property;
}
if(is_array($v)) {
$value = array_merge($value, $v);
} else {
$value[] = (string) $v;
}
}
$matches = $selector->matches($value);
if($matches) return true;
// attempt any alternate operators, if present
foreach($selector->altOperators as $altOperator) {
$altSelector = self::getSelectorByOperator($altOperator);
if(!$altSelector) continue;
$this->wire($altSelector);
$selector->copyTo($altSelector);
$matches = $altSelector->matches($value);
if($matches) break;
}
return $matches;
}
/**
* Do the given OR-groups match the given Wire?
*
* @param array|string[]|array[] $orGroups
* @param Wire $item
* @return bool
* @since 3.0.330
*
*/
protected function matchesOrGroups(array $orGroups, Wire $item) {
$matches = true;
foreach($orGroups as $selectorStrings) {
$orGroupMatches = false;
foreach($selectorStrings as $s) {
/** @var Selectors $orGroupSelectors */
$orGroupSelectors = $this->wire(new Selectors($s));
if(!$orGroupSelectors->matches($item)) continue;
$orGroupMatches = true;
break;
}
if(!$orGroupMatches) {
$matches = false;
break;
}
}
return $matches;
}

View File

@@ -381,7 +381,7 @@ class Session extends Wire implements \IteratorAggregate {
// if valid, update last request time
$this->set('_user', 'ts', time());
} else if($reason && $userID && $userID != $this->wire('config')->guestUserPageID) {
} else if($reason && $userID && $userID != $this->config->guestUserPageID) {
// otherwise log the invalid session
$user = $this->wire()->users->get((int) $userID);
if($user && $user->id) $reason = "User '$user->name' - $reason";
@@ -1706,7 +1706,7 @@ class Session extends Wire implements \IteratorAggregate {
* @since 3.0.166
*
*/
public function sessionHandler(WireSessionHandler $sessionHandler = null) {
public function sessionHandler(?WireSessionHandler $sessionHandler = null) {
if($sessionHandler) $this->sessionHandler = $sessionHandler;
return $this->sessionHandler;
}

View File

@@ -59,8 +59,8 @@
* @property string $sortfield Field that children of templates using this page should sort by (leave blank to let page decide, or specify "sort" for manual drag-n-drop). #pw-group-family
* @property int $noChildren Set to 1 to cancel use of childTemplates. #pw-group-family
* @property int $noParents Set to 1 to cancel use of parentTemplates, set to -1 to only allow one page using this template to exist. #pw-group-family
* @property array $childTemplates Array of template IDs that are allowed for children. Blank array indicates "any". #pw-group-family
* @property array $parentTemplates Array of template IDs that are allowed for parents. Blank array indicates "any". #pw-group-family
* @property int[] $childTemplates Array of template IDs that are allowed for children. Blank array indicates "any". #pw-group-family
* @property int[] $parentTemplates Array of template IDs that are allowed for parents. Blank array indicates "any". #pw-group-family
* @property string $childNameFormat Name format for child pages. when specified, the page-add UI step can be skipped when adding children. Counter appended till unique. Date format assumed if any non-pageName chars present. Use 'title' to pull from title field. #pw-group-family
*
* URLs
@@ -1171,7 +1171,6 @@ class Template extends WireData implements Saveable, Exportable {
* Given an array of export data, import it
*
* @param array $data
* @return bool True if successful, false if not
* @return array Returns array(
* [property_name] => array(
* 'old' => 'old value', // old value (in string comparison format)
@@ -1222,7 +1221,7 @@ class Template extends WireData implements Saveable, Exportable {
* #pw-group-family
*
* @param array|TemplatesArray|null $setValue Specify only when setting, an iterable value containing Template objects, IDs or names
* @return TemplatesArray
* @return TemplatesArray|Template[]
* @since 3.0.153
*
*/
@@ -1237,7 +1236,7 @@ class Template extends WireData implements Saveable, Exportable {
*
* @param string $property Specify either 'childTemplates' or 'parentTemplates'
* @param array|TemplatesArray|null $setValue Iterable value containing Template objects, IDs or names
* @return TemplatesArray
* @return TemplatesArray|Template[]
* @since 3.0.153
*
*/
@@ -1267,6 +1266,7 @@ class Template extends WireData implements Saveable, Exportable {
if($template) $value->add($template);
}
}
/** @var TemplatesArray|Template[] $value */
return $value;
}
@@ -1299,8 +1299,8 @@ class Template extends WireData implements Saveable, Exportable {
* This is based on family settings, when applicable.
* It also takes into account user access, if requested (see arg 1).
*
* If there is no shortcut parent, NULL is returned.
* If there are multiple possible shortcut parents, a NullPage is returned.
* If there is no defined parent, NULL is returned.
* If there are multiple defined parents, a NullPage is returned.
*
* #pw-group-family
*
@@ -1313,7 +1313,7 @@ class Template extends WireData implements Saveable, Exportable {
}
/**
* Return all possible parent pages for this template
* Return all defined parent pages for this template
*
* #pw-group-family
*
@@ -1617,5 +1617,3 @@ class Template extends WireData implements Saveable, Exportable {
}
}

View File

@@ -609,14 +609,20 @@ class TemplateFile extends WireData {
*
* USAGE from template file is: return $this->halt();
*
* @param bool $halt
* @param bool|string $halt
* If given boolean, it will set the halt status.
* If given string, it will be output (3.0.239+)
* @return $this
*
*/
protected function halt($halt = true) {
$this->halt = $halt ? true : false;
public function halt($halt = true) {
if(is_bool($halt)) {
$this->halt = $halt ? true : false;
} else if(is_string($halt)) {
$this->halt = true;
echo $halt;
}
return $this;
}
}

View File

@@ -5,7 +5,7 @@
*
* Manages and provides access to all the Template instances
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* #pw-summary Manages and provides access to all the Templates.
@@ -693,8 +693,8 @@ class Templates extends WireSaveableItems {
*
* - This is based on family settings, when applicable.
* - It also takes into account user access, if requested (see arg 1).
* - If there is no shortcut parent, NULL is returned.
* - If there are multiple possible shortcut parents, a NullPage is returned.
* - If there is no defined parent, NULL is returned.
* - If there are multiple defined parents, a NullPage is returned (use $getAll to get them).
*
* @param Template $template
* @param bool $checkAccess Whether or not to check for user access to do this (default=false).
@@ -706,84 +706,63 @@ class Templates extends WireSaveableItems {
public function getParentPage(Template $template, $checkAccess = false, $getAll = false) {
$pages = $this->wire()->pages;
$user = $this->wire()->user;
$foundParent = null;
$foundParents = $getAll ? $pages->newPageArray() : null;
$foundParentQty = 0;
$foundParents = $pages->newPageArray();
$maxStatus = is_int($getAll) && $getAll ? ($getAll * 2) : 0;
$earlyExit = false;
if($template->noShortcut || !count($template->parentTemplates)) return $foundParents;
if($template->noParents == -1) {
// only 1 page of this type allowed
if($this->getNumPages($template) > 0) return $foundParents;
if($this->getNumPages($template) > 0) $earlyExit = true;
} else if($template->noParents == 1) {
return $foundParents;
// no parents allowed
$earlyExit = true;
} else if(!count($template->parentTemplates)) {
// no parent templates defined
$earlyExit = true;
}
if($earlyExit) return $getAll ? $foundParents : null;
$childTestPage = $checkAccess ? $pages->newPage($template) : null;
foreach($template->parentTemplates as $parentTemplateID) {
$parentTemplate = $this->get((int) $parentTemplateID);
if(!$parentTemplate) continue;
// if parent template does not exist or not allow children, skip it
if(!$parentTemplate || $parentTemplate->noChildren) continue;
// if the parent template doesn't have this as an allowed child template, exclude it
if($parentTemplate->noChildren) continue;
// if the parent template doesn't have this as an allowed child template, skip it
if(!in_array($template->id, $parentTemplate->childTemplates)) continue;
// sort=status ensures that a non-hidden page is given preference to a hidden page
$include = $checkAccess ? "unpublished" : "all";
$selector = "templates_id=$parentTemplate->id, include=$include, sort=status";
if($maxStatus) {
$selector .= ", status<$maxStatus";
} else if(!$getAll) {
} else if(!$getAll && !$checkAccess) {
$selector .= ", limit=2";
}
$parentPages = $pages->find($selector);
$numParentPages = count($parentPages);
// undetermined parent
if(!$numParentPages) continue;
if($getAll) {
// build list of all parents (will check access outside loop)
$foundParents->add($parentPages);
continue;
} else if($numParentPages > 1) {
// multiple possible parents, we can early-exit
$foundParentQty += $numParentPages;
break;
} else {
// one possible parent
$parentPage = $parentPages->first();
foreach($pages->find($selector) as $parentPage) {
if($checkAccess && !$parentPage->addable($childTestPage)) continue;
$foundParents->add($parentPage);
$earlyExit = !$getAll && $foundParents->count() > 1;
if($earlyExit) break;
}
if($checkAccess) {
if($parentPage->id) {
// single defined parent
$p = $pages->newPage($template);
if(!$parentPage->addable($p)) continue;
} else {
// multiple possible parents
if(!$user->hasPermission('page-create', $template)) continue;
}
}
if($parentPage && $parentPage->id) $foundParentQty++;
$foundParent = $parentPage;
if($foundParentQty > 1) break;
if($earlyExit) break;
}
if($checkAccess && $getAll && $foundParents && $foundParents->count()) {
$p = $pages->newPage($template);
foreach($foundParents as $parentPage) {
if(!$parentPage->addable($p)) $foundParents->remove($parentPage);
}
}
if($getAll) return $foundParents; // always returns PageArray (populated or not)
if($getAll) return $foundParents;
if($foundParentQty > 1) return $pages->newNullPage();
$qty = $foundParents->count();
if($qty > 1) return $pages->newNullPage(); // multiple possible parents
if($qty === 1) return $foundParents->first(); // one possible parent
return $foundParent;
return null; // no parents
}
/**
@@ -872,11 +851,24 @@ class Templates extends WireSaveableItems {
// determine if custom class available (3.0.152+)
if($usePageClasses) {
// generate a CamelCase name + 'Page' from template name, i.e. 'blog-post' => 'BlogPostPage'
$className = ucwords(str_replace(array('-', '_', '.'), ' ', $template->name));
$className = __NAMESPACE__ . "\\" . str_replace(' ', '', $className) . 'Page';
if(class_exists($className) && wireInstanceOf($className, $corePageClass)) {
$pageClass = $className;
$customPageClass = '';
// repeaters support a field-name based name strategy
/** @var RepeaterField $field */
if(strpos($template->name, 'repeater_') === 0) {
$field = $this->wire()->fields->get(ltrim(strstr($template->name, '_'), '_'));
if($field && wireInstanceOf($field->type, 'FieldtypeRepeater')) {
$customPageClass = $field->type->getCustomPageClass($field);
}
}
if($customPageClass) {
$pageClass = $customPageClass;
} else {
// generate a CamelCase name + 'Page' from template name, i.e. 'blog-post' => 'BlogPostPage'
$className = ucwords(str_replace(array('-', '_', '.'), ' ', $template->name));
$className = __NAMESPACE__ . "\\" . str_replace(' ', '', $className) . 'Page';
if(class_exists($className) && wireInstanceOf($className, $corePageClass)) {
$pageClass = $className;
}
}
}
@@ -1155,4 +1147,3 @@ class Templates extends WireSaveableItems {
*/
}

View File

@@ -5,7 +5,7 @@
*
* WireArray of Template instances as used by Templates class.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
*/
@@ -21,6 +21,7 @@ class TemplatesArray extends WireArray {
}
public function getItemKey($item) {
/** @var Template $item */
return $item->id;
}

View File

@@ -484,11 +484,11 @@ class Tfa extends WireData implements Module, ConfigurableModule {
/**
* Get the TFA module for given user or current session
*
* @param User $user Optionally specify user
* @param User|null $user Optionally specify user
* @return Tfa|null
*
*/
public function getModule(User $user = null) {
public function getModule(?User $user = null) {
$module = null;
$moduleName = $this->sessionGet('type');
@@ -599,6 +599,7 @@ class Tfa extends WireData implements Module, ConfigurableModule {
$f->attr('name', 'tfa_code');
$f->label = $this->inputLabel; // Authentication code
$f->attr('required', 'required');
$f->attr('autocomplete', 'one-time-code');
$f->collapsed = Inputfield::collapsedNever;
$form->add($f);
@@ -913,13 +914,13 @@ class Tfa extends WireData implements Module, ConfigurableModule {
* Modules that support auto-enable must implement this method to return true. Modules
* that do not support it can ignore this method, as the default returns false.
*
* @param User $user Specify user to also confirm it is supported for given user.
* @param User|null $user Specify user to also confirm it is supported for given user.
* Omit to test if the module supports it in general.
* @return bool
* @since 3.0.160
*
*/
public function autoEnableSupported(User $user = null) {
public function autoEnableSupported(?User $user = null) {
if($user && $this->className() !== 'Tfa') {
// if it doesn't support it without user, then exit now
if(!$this->autoEnableSupported()) return false;
@@ -1901,7 +1902,7 @@ class RememberTfa extends Wire {
* @return string
*
*/
protected function serverValue($cookieValue, User $user = null) {
protected function serverValue($cookieValue, ?User $user = null) {
if($user === null) $user = $this->user;
return sha1(
$user->id . $user->name . $user->email .
@@ -1954,11 +1955,11 @@ class RememberTfa extends Wire {
/**
* Get fingerprint string
*
* @param array $types Fingerprints to use, or omit when creating new
* @param array|null $types Fingerprints to use, or omit when creating new
* @return string
*
*/
public function getFingerprintString(array $types = null) {
public function getFingerprintString(?array $types = null) {
if($types === null) $types = $this->fingerprints;
return implode(',', $types) . ':' . sha1(implode("\n", $this->getFingerprintArray()));
}

View File

@@ -41,10 +41,10 @@ class User extends Page {
/**
* Create a new User page in memory.
*
* @param Template $tpl Template object this page should use.
* @param Template|null $tpl Template object this page should use.
*
*/
public function __construct(Template $tpl = null) {
public function __construct(?Template $tpl = null) {
if(!$tpl) $this->template = $this->wire()->templates->get('user');
$this->_parent_id = $this->wire()->config->usersPageID;
parent::__construct($tpl);
@@ -234,11 +234,11 @@ class User extends Page {
* #pw-group-access
*
* @param string|Permission
* @param Page $page Optional page to check against
* @param Page|null $page Optional page to check against
* @return bool
*
*/
protected function ___hasPagePermission($name, Page $page = null) {
protected function ___hasPagePermission($name, ?Page $page = null) {
if($this->isSuperuser()) return true;
$permissions = $this->wire()->permissions;
@@ -404,11 +404,11 @@ class User extends Page {
*
* #pw-group-access
*
* @param Page $page Optional page to check against
* @param Page|null $page Optional page to check against
* @return PageArray of Permission objects
*
*/
public function getPermissions(Page $page = null) {
public function getPermissions(?Page $page = null) {
// Does not currently include page-add or page-create permissions (runtime).
if($this->isSuperuser()) return $this->wire()->permissions->getIterator(); // all permissions
$userPermissions = $this->wire()->pages->newPageArray();

View File

@@ -14,6 +14,8 @@
* @method void added($user) Hook called just after a User is added #pw-hooker
* @method void deleteReady($user) Hook called before a User is deleted #pw-hooker
* @method void deleted($user) Hook called after a User is deleted #pw-hooker
* @method User new($options = []) Create new User instance in memory (3.0.249+)
*
*
*/
@@ -156,9 +158,11 @@ class Users extends PagesType {
*
*/
public function newUser() {
$config = $this->wire()->config;
/** @var User $user */
$user = $this->wire()->pages->newPage(array(
'template' => 'user',
'template' => $this->wire()->templates->get($config->userTemplateID),
'parent' => $config->usersPageID,
'pageClass' => $this->getPageClass()
));
return $user;

View File

@@ -626,7 +626,7 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
* @deprecated
*
*/
static public function isHooked($method, Wire $instance = null) {
static public function isHooked($method, ?Wire $instance = null) {
/** @var ProcessWire $wire */
$wire = $instance ? $instance->wire() : ProcessWire::getCurrentInstance();
if($instance) return $instance->wire()->hooks->hasHook($instance, $method);
@@ -1370,7 +1370,7 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
*
* #pw-hooker
*
* @param \Exception|WireException $e Exception object that was thrown.
* @param \Exception $e Exception object that was thrown.
* @param bool|int $severe Whether or not it should be considered severe (default=true).
* @param string|array|object|true $text Additional details (optional):
* - When provided, it will be sent to `$this->error($text)` if $severe is true, or `$this->warning($text)` if $severe is false.
@@ -1758,7 +1758,7 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
* @param string|object $name Name of API variable to retrieve, set, or omit to retrieve the master ProcessWire object.
* @param null|mixed $value Value to set if using this as a setter, otherwise omit.
* @param bool $lock When using as a setter, specify true if you want to lock the value from future changes (default=false).
* @return ProcessWire|Wire|Session|Page|Pages|Modules|User|Users|Roles|Permissions|Templates|Fields|Fieldtypes|Sanitizer|Config|Notices|WireDatabasePDO|WireHooks|WireDateTime|WireFileTools|WireMailTools|WireInput|string|mixed
* @return ProcessWire|Wire|Session|Page|Pages|Modules|User|Users|Roles|Permissions|Templates|Fields|Fieldtypes|Sanitizer|Config|Notices|WireDatabasePDO|WireHooks|WireDateTime|WireFileTools|WireMailTools|WireInput|PagesVersions|string|mixed
* @throws WireException
*
*
@@ -1912,4 +1912,3 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
}

View File

@@ -11,11 +11,10 @@
*
* @todo can we implement next() and prev() like on Page, as alias to getNext() and getPrev()?
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* @method WireArray and($item)
* @method static WireArray new($items = array())
* @property int $count Number of items
* @property Wire|null $first First item
* @property Wire|null $last Last item
@@ -88,12 +87,47 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
*/
protected $sortFlags = 0; // 0 == SORT_REGULAR
/**
* For WireArray that holds WireData objects, property that contains the items name
*
* @var string
* @since 3.0.240
*
*/
protected $nameProperty = 'name';
/**
* Is this WireArray indexed by the name property?
*
* This will be auto-detected at runtime unless specifically set in the constructor.
*
* @var bool|null Bool once known, null if not yet known
* @since 3.0.240
*
*/
protected $indexedByName = null;
/**
* Does this WireArray use numeric keys?
*
* This will be auto-detected at runtime unless specifically set in the constructor.
*
* @var bool|null
* @since 3.0.240
*
*/
protected $usesNumericKeys = null;
/**
* Construct
*
*/
public function __construct() {
if($this->className() === 'WireArray') $this->duplicateChecking = false;
if($this->className() === 'WireArray') {
$this->duplicateChecking = false;
$this->indexedByName = false;
$this->usesNumericKeys = true;
}
parent::__construct();
}
@@ -296,7 +330,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
$key = key($this->data);
}
$this->trackChange("add", null, $item);
if($this->trackChanges) $this->trackChange("add", null, $item);
$this->trackAdd($item, $key);
return $this;
@@ -461,7 +495,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
throw new WireException("Key '$key' is not an allowed key for " . get_class($this));
}
$this->trackChange($key, isset($this->data[$key]) ? $this->data[$key] : null, $value);
if($this->trackChanges) $this->trackChange($key, isset($this->data[$key]) ? $this->data[$key] : null, $value);
$this->data[$key] = $value;
$this->trackAdd($value, $key);
return $this;
@@ -602,7 +636,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
if(isset($this->data[$k])) {
$match = $this->data[$k];
} else if($numericKeys) {
$match = $this->getItemThatMatches('name', $k);
$match = $this->getItemThatMatches($this->nameProperty, $k);
}
if($match) break;
}
@@ -613,7 +647,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
// if the WireArray uses numeric keys, then it's okay to
// match a 'name' field if the provided key is a string
if($this->usesNumericKeys()) {
$match = $this->getItemThatMatches('name', $key);
$match = $this->getItemThatMatches($this->nameProperty, $key);
}
}
@@ -760,7 +794,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
$match = $this->findOne($key);
} else if($this->usesNumericKeys()) {
$match = $this->getItemThatMatches('name', $key);
$match = $this->getItemThatMatches($this->nameProperty, $key);
}
}
@@ -997,7 +1031,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
reset($this->data);
$key = key($this->data);
}
$this->trackChange('prepend', null, $item);
if($this->trackChanges) $this->trackChange('prepend', null, $item);
$this->trackAdd($item, $key);
return $this;
}
@@ -1058,7 +1092,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
$key = key($this->data);
$item = array_shift($this->data);
if(is_null($item)) return null;
$this->trackChange('shift', $item, null);
if($this->trackChanges) $this->trackChange('shift', $item, null);
$this->trackRemove($item, $key);
return $item;
}
@@ -1094,7 +1128,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
$key = key($this->data);
$item = array_pop($this->data);
if(is_null($item)) return null;
$this->trackChange('pop', $item, null);
if($this->trackChanges) $this->trackChange('pop', $item, null);
$this->trackRemove($item, $key);
return $item;
}
@@ -1119,7 +1153,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
$data[$key] = $this->data[$key];
}
$this->trackChange('shuffle', $this->data, $data);
if($this->trackChanges) $this->trackChange('shuffle', $this->data, $data);
$this->data = $data;
@@ -1219,7 +1253,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
if(array_key_exists($key, $this->data)) {
$item = $this->data[$key];
unset($this->data[$key]);
$this->trackChange("remove", $item, null);
if($this->trackChanges) $this->trackChange("remove", $item, null);
$this->trackRemove($item, $key);
} else if(!$obj && is_string($key) && Selectors::stringHasSelector($key)) {
foreach($this->find($key) as $item) {
@@ -1329,6 +1363,13 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
if(!$isArray) $properties = explode(',', $properties);
if(empty($properties)) return $this;
if($propertiesStr === $this->nameProperty && $this->indexedByName) {
// optimization when it's a very simple sort by name
ksort($this->data, $this->sortFlags);
if($this->trackChanges) $this->trackChange("sort:$propertiesStr");
return $this;
}
// shortcut for random (only allowed as the sole sort property)
// no warning/error for issuing more properties though
@@ -1611,9 +1652,11 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
if(is_array($v)) $v = implode(' ', $this->wire()->sanitizer->flatArray($v));
$value[] = (string) $v;
}
} else {
} else if($item instanceof Wire) {
$value = $this->getItemPropertyValue($item, $selector->field);
$value = is_array($value) ? $this->wire()->sanitizer->flatArray($value) : (string) $value;
} else {
$value = $item; // integer, string, etc. (non-Wire object)
}
if($not === $selector->matches($value) && isset($this->data[$key])) {
$qtyMatch++;
@@ -1934,13 +1977,15 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
*
*/
public function __toString() {
$s = '';
foreach($this as $value) {
$values = array();
foreach($this->data as $value) {
if(is_array($value)) $value = "array(" . count($value) . ")";
$s .= "$value|";
$value = (string) $value;
if(!strlen($value)) continue;
if(strpos($value, '|') !== false) $value = str_replace('|', ' ', $value);
$values[] = $value;
}
$s = rtrim($s, '|');
return $s;
return implode('|', $values);
}
/**
@@ -1998,7 +2043,20 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
*
*/
protected function trackAdd($item, $key) {
if($key) {}
if($key !== null && $key !== false) {
if($this->usesNumericKeys === null) {
$this->usesNumericKeys = is_int($key);
}
if($this->indexedByName === null) {
$this->indexedByName = false;
if($item instanceof WireData) {
$name = $item->get($this->nameProperty);
if($name === $key && isset($this->data[$name]) && $this->data[$name] === $item) {
$this->indexedByName = true;
}
}
}
}
if($this->trackChanges()) $this->itemsAdded[] = $item;
// wire this WireArray to the same instance of $item, if it isnt already wired
if($this->_wire === null && $item instanceof Wire && $item->isWired()) $item->wire($this);
@@ -2116,16 +2174,29 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
*/
protected function usesNumericKeys() {
static $testItem = null;
static $usesNumericKeys = null;
if($this->usesNumericKeys !== null) {
return $this->usesNumericKeys;
}
if(!empty($this->data)) {
reset($this->data);
$key = key($this->data);
if($key !== null) {
$this->usesNumericKeys = is_int($key);
return $this->usesNumericKeys;
}
}
$testItem = $this->makeBlankItem();
if($testItem === null) {
$this->usesNumericKeys = true;
} else {
$key = $this->getItemKey($testItem);
$this->usesNumericKeys = is_int($key);
}
if(!is_null($usesNumericKeys)) return $usesNumericKeys;
if(is_null($testItem)) $testItem = $this->makeBlankItem();
if(is_null($testItem)) return true;
$key = $this->getItemKey($testItem);
$usesNumericKeys = is_int($key) ? true : false;
return $usesNumericKeys;
return $this->usesNumericKeys;
}
/**
@@ -2550,7 +2621,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
}
} else {
// array or string or null
if(is_null($func)) $func = 'name';
if(is_null($func)) $func = $this->nameProperty;
$result = $this->explode($func);
}
@@ -2656,7 +2727,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
$item = $item->debugInfoSmall();
} else if($item instanceof WireData) {
$_item = $item;
$item = $item->get('name');
$item = $item->get($this->nameProperty);
if(!$item) $item = $_item->get('id');
if(!$item) $item = $_item->className();
} else {

View File

@@ -602,8 +602,10 @@ class WireCache extends Wire {
*
*/
public function getExpires($expire, $verbose = null) {
if($expire instanceof Wire && $expire->id) {
$isString = is_string($expire);
if(!$isString && $expire instanceof Wire && $expire->id) {
if($expire instanceof Page) {
// page object
@@ -618,7 +620,7 @@ class WireCache extends Wire {
$expire = time() + self::expireDaily;
}
} else if(is_array($expire)) {
} else if(!$isString && is_array($expire)) {
// expire value already prepared by a previous call, just return it
if(isset($expire['selector']) && isset($expire['expire'])) {
if($verbose || $verbose === null) return $expire; // return array
@@ -628,11 +630,11 @@ class WireCache extends Wire {
$expire = self::expireDaily;
}
} else if(is_string($expire) && isset($this->expireNames[$expire])) {
} else if($isString && isset($this->expireNames[$expire])) {
// named expiration constant like "hourly", "daily", etc.
$expire = time() + $this->expireNames[$expire];
} else if(is_string($expire) && Selectors::stringHasSelector($expire)) {
} else if($isString && Selectors::stringHasSelector($expire)) {
// expire when page matches selector
if($verbose || $verbose === null) {
return array(
@@ -649,7 +651,7 @@ class WireCache extends Wire {
} else {
// account for date format as string
if(is_string($expire) && !ctype_digit("$expire")) {
if($isString && !ctype_digit("$expire")) {
$expire = strtotime($expire);
$isDate = true;
} else {
@@ -870,7 +872,7 @@ class WireCache extends Wire {
if(!$forceRun) {
// run general maintenance only once every 10 minutes
$filename = $this->wire()->config->paths->cache . 'WireCache.maint';
if(@filemtime($filename) > (time() - 600)) return false;
if(file_exists($filename) && filemtime($filename) > (time() - 600)) return false;
touch($filename);
}

View File

@@ -3,7 +3,7 @@
/**
* Database cache handler for WireCache
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* @since 2.0.218
@@ -205,6 +205,38 @@ class WireCacheDatabase extends Wire implements WireCacheInterface {
return $result;
}
/**
* Database cache maintenance (every 10 minutes)
*
* @param Template|Page $obj
* @return bool
* @throws WireException
* @since 3.0.242
*
*/
public function maintenance($obj) {
if($obj) return false; // let WireCache handle when object value is provided
$sql =
'DELETE FROM caches ' .
'WHERE (expires<=:now AND expires>:never) ' .
'OR expires<:then';
$query = $this->wire()->database->prepare($sql);
$query->bindValue(':now', date(WireCache::dateFormat, time()));
$query->bindValue(':never', WireCache::expireNever);
$query->bindValue(':then', '1974-10-10 10:10:10');
$query->execute();
$qty = $query->rowCount();
if($qty) $this->wire->cache->log(
sprintf('DB cache maintenance expired %d cache(s)', $qty)
);
return $qty > 0;
}
/**
* Create the caches table if it happens to have been deleted
*

View File

@@ -6,7 +6,7 @@
* A WireData object that maintains its data in a database table rather than just in memory.
* An example of usage is the `$page->meta()` method.
*
* ProcessWire 3.x, Copyright 2019
* ProcessWire 3.x, Copyright 2023
* https://processwire.com
*
*/
@@ -215,7 +215,7 @@ class WireDataDB extends WireData implements \Countable {
$sql =
"INSERT INTO `$table` (source_id, name, data) VALUES(:source_id, :name, :data) " .
"ON DUPLICATE KEY UPDATE source_id=VALUES(source_id), name=VALUES(name), data=VALUES(data)";
$query = $this->wire('database')->prepare($sql);
$query = $this->wire()->database->prepare($sql);
$query->bindValue(':source_id', $this->sourceID(), \PDO::PARAM_INT);
$query->bindValue(':name', $name);
$query->bindValue(':data', $data);

View File

@@ -49,7 +49,7 @@
* ~~~~~
* #pw-body
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
*
@@ -834,10 +834,14 @@ class WireDatabaseBackup {
if(in_array($table, $options['excludeExportTables'])) continue;
$numTables++;
$columns = array();
$columnTypes = array();
$query = $database->prepare("SHOW COLUMNS FROM `$table`");
$query->execute();
/** @noinspection PhpAssignmentInConditionInspection */
while($row = $query->fetch(\PDO::FETCH_NUM)) $columns[] = $row[0];
while($row = $query->fetch(\PDO::FETCH_NUM)) {
$columns[] = $row[0];
$columnTypes[] = $row[1];
}
$query->closeCursor();
$columnsStr = '`' . implode('`, `', $columns) . '`';
@@ -862,9 +866,12 @@ class WireDatabaseBackup {
while($row = $query->fetch(\PDO::FETCH_NUM)) {
$numInserts++;
$out = "\nINSERT INTO `$table` ($columnsStr) VALUES(";
foreach($row as $value) {
foreach($row as $key => $value) {
$columnType = $columnTypes[$key];
if(is_null($value)) {
$value = 'NULL';
} else if(stripos($columnType, 'bit') === 0 && ctype_digit("$value")) {
// leave bit column value unquoted
} else {
if($hasReplace) foreach($options['findReplace'] as $find => $replace) {
if(strpos($value, $find)) $value = str_replace($find, $replace, $value);

View File

@@ -475,14 +475,58 @@ class WireDatabasePDO extends Wire implements WireDatabase {
}
}
/**
* Reset the current PDO connection(s)
*
* This forces re-creation of the PDO instance(s), whether writer, reader or both.
* This may be useful to call after a "MySQL server has gone away" error to attempt
* to re-establish the connection.
*
* #pw-group-connection
*
* @param string|null $type
* - Specify 'writer' to reset writer instance.
* - Specify 'reader' to reset reader instance.
* - Omit or null to reset both, or whichever one is in use.
* @return self
* @since 3.0.240
*
*/
public function reset($type = null) {
$this->close($type);
$this->pdo($type);
return $this;
}
/**
* Close the current PDO connection(s)
*
* #pw-internal
*
* @param string|null $type
* - Specify 'writer' to close writer instance.
* - Specify 'reader' to close reader instance.
* - Omit or null to close both.
* @return self
* @since 3.0.240
*
*/
public function close($type = null) {
if($type === 'reader' || $type === null) {
$this->reader['pdo'] = null;
}
if($type === 'writer' || $type === null) {
$this->writer['pdo'] = null;
}
return $this;
}
/**
* Return the actual current PDO connection instance
*
* If connection is lost, this will restore it automatically.
* #pw-internal
*
* #pw-group-connection
*
* @param string|\PDOStatement|null SQL, statement, or statement type (reader or primary) (3.0.175+)
* @param string|\PDOStatement|null SQL, statement, or statement type (reader or writer) (3.0.175+)
*
* @return \PDO
*
@@ -593,6 +637,14 @@ class WireDatabasePDO extends Wire implements WireDatabase {
} else if(stripos($statement, 'select') === 0) {
// select query is always reader
$type = $reader;
// check that this is not an InnoDB 'SELECT' '… FOR UPDATE' or '… FOR SHARE' query
$forpos = $this->engine === 'innodb' ? strripos($query, 'for') : 0;
if($forpos) {
$for = ltrim(strtolower(substr($query, $forpos+4, 15)));
if(stripos($for, 'update') === 0 || stripos($for, 'share') === 0) {
$type = $writer;
}
}
} else if(stripos($statement, 'insert') === 0) {
// insert query is always writer
$type = $writer;
@@ -810,6 +862,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
*
*/
public function commit() {
if(!$this->inTransaction()) return false;
$this->allowReader(true);
return $this->pdoWriter()->commit();
}
@@ -824,6 +877,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
*
*/
public function rollBack() {
if(!$this->inTransaction()) return false;
$this->allowReader(true);
return $this->pdoWriter()->rollBack();
}
@@ -925,31 +979,41 @@ class WireDatabasePDO extends Wire implements WireDatabase {
*
* @param \PDOStatement $query
* @param bool $throw Whether or not to throw exception on query error (default=true)
* @param int $maxTries Deprecated/argument does nothing (was: “Max number of times it will attempt to retry query on error”)
* @param int $maxTries Max number of times it will attempt to retry query on lost connection error
* @return bool True on success, false on failure. Note if you want this, specify $throw=false in your arguments.
* @throws \PDOException
*
*/
public function execute(\PDOStatement $query, $throw = true, $maxTries = 3) {
$tries = 0;
try {
$result = $query->execute();
} catch(\PDOException $e) {
$result = false;
if($query->errorCode() == '42S22') {
// unknown column error
$errorInfo = $query->errorInfo();
if(preg_match('/[\'"]([_a-z0-9]+\.[_a-z0-9]+)[\'"]/i', $errorInfo[2], $matches)) {
$this->unknownColumnError($matches[1]);
do {
$tryAgain = false;
try {
$result = $query->execute();
} catch(\PDOException $e) {
$result = false;
if($query->errorCode() == '42S22') {
// unknown column error
$errorInfo = $query->errorInfo();
if(preg_match('/[\'"]([_a-z0-9]+\.[_a-z0-9]+)[\'"]/i', $errorInfo[2], $matches)) {
$this->unknownColumnError($matches[1]);
}
} else if($e->getCode() === 'HY000' && $tries < $maxTries) {
// mysql server has gone away
$this->reset();
$tryAgain = true;
$tries++;
}
if($tryAgain) {
// we will try again on next iteration
} else if($throw) {
throw $e;
} else {
$this->error($e->getMessage());
}
}
if($throw) {
throw $e;
} else {
$this->error($e->getMessage());
}
if($maxTries) {} // ignore, argument no longer used
}
} while($tryAgain);
return $result;
}
@@ -1907,4 +1971,3 @@ class WireDatabasePDO extends Wire implements WireDatabase {
}
}

View File

@@ -421,7 +421,7 @@ class WireDateTime extends Wire {
$value = $this->relativeTimeStr($ts, 1, false);
} else if($format == 'ts') {
$value = $ts;
} else if(strpos($format, '%') !== false && version_compare(PHP_VERSION, '8.1.0', '<')) {
} else if(strpos($format, '%') !== false) {
$value = $this->strftime($format, $ts);
} else {
$value = date($format, $ts);
@@ -435,13 +435,16 @@ class WireDateTime extends Wire {
* Parse about any English textual datetime description into a Unix timestamp using PHPs strtotime()
*
* This function behaves the same as PHPs version except that it optionally accepts an `$options` array
* and lets you specify the return value for empty or zeroed dates like 0000-00-00. If given a zerod date
* then it returns null by default (rather than throwing an error as PHP8 does).
* and lets you specify the return value for empty or zeroed dates like 0000-00-00. If given a zerod date
* then it returns null by default (rather than throwing an error as PHP8 does). As of 3.0.238+ this method
* also lets you optionally specify an input format should the given date string not be strtotime compatible.
*
* @param string $str Date/time string
* @param array|int $options Options to modify behavior, or specify int for the `baseTimestamp` option.
* @param array|int $options Options to modify behavior, or specify int for the `baseTimestamp` option, or string for `inputFormat` option.
* - `emptyReturnValue` (int|null|false): Value to return for empty or zero-only date strings (default=null)
* - `baseTimestamp` (int|null): The timestamp which is used as a base for the calculation of relative dates.
* - `inputFormat` (string): Optional date format that given $str is in, if not strtotime() compatible. (3.0.238+)
* - `outputFormat` (string): Optionally return value in this date format rather than unix timestamp (3.0.238+)
* @return false|int|null
* @see https://www.php.net/manual/en/function.strtotime.php
* @since 3.0.178
@@ -451,17 +454,110 @@ class WireDateTime extends Wire {
$defaults = array(
'emptyReturnValue' => null,
'baseTimestamp' => null,
'inputFormat' => '',
'outputFormat' => '',
);
if(is_int($options)) $defaults['baseTimestamp'] = $options;
$options = is_array($options) ? array_merge($defaults, $options) : $defaults;
if(!empty($options['outputFormat'])) return $this->strtodate($str, $options);
$str = trim($str);
if(empty($str)) return $options['emptyReturnValue'];
if(strpos($str, '00') === 0) {
$test = trim(preg_replace('/[^\d]/', '', $str), '0');
if(!strlen($test)) return $options['emptyReturnValue'];
}
if($options['baseTimestamp'] === null) return strtotime($str);
return strtotime($str, $options['baseTimestamp']) ;
if($options['inputFormat']) {
$value = \DateTimeImmutable::createFromFormat($options['inputFormat'], $str);
$value = $value ? $value->getTimestamp() : false;
} else {
$value = strtotime($str, $options['baseTimestamp']) ;
}
if($value === false) $value = $options['emptyReturnValue'];
return $value;
}
/**
* Parse English textual datetime description into a formatted date string, or blank if not a date
*
* @param string $str Date/time string to parse
* @param string|array $format Output format to use, or array for $options.
* - Omit or boolean true for default 'Y-m-d H:i:s'.
* - Specify date format string, see [formats](https://www.php.net/manual/en/datetime.format.php).
* - Specify boolean false for unix timestamp.
* - Specify array of options.
* @param array $options Can also be specified as 2nd argument. Options include:
* - `emptyReturnValue` (int|null|false): Value to return for empty or zero-only date strings (default='')
* - `baseTimestamp` (int|null): The timestamp which is used as a base for the calculation of relative dates.
* - `inputFormat` (string): Optional date format that given $str is in, if not strtotime() compatible.
* - `outputFormat` (string|bool): Format to return date string in, used only if $options specified for $format argument.
* - `format` (string|bool) Optional alias of outputFormat, used only if $options specified for $format argument.
* @return string Return string, returns blank string on fail.
* @since 3.0.238
*
*/
public function strtodate($str, $format = true, array $options = array()) {
$defaults = array(
'emptyReturnValue' => '',
'baseTimestamp' => null,
'outputFormat' => 'Y-m-d H:i:s',
'inputFormat' => '',
);
if(is_array($format)) {
$options = array_merge($defaults, $format);
if(isset($options['format'])) $options['outputFormat'] = $options['format'];
$format = $options['outputFormat'];
} else {
$options = array_merge($defaults, $options);
}
$is = false;
$str = trim((string) $str);
$len = strlen($str);
if($format === true) $format = $defaults['outputFormat'];
if(!$len || $len > 30) {
return $options['emptyReturnValue'];
} else if(ctype_digit($str) && strpos($str, '0') !== 0) {
if($len === 1) $str = "0$str";
if($len === 2) {
$value = strtotime("20$str-01-01");
} else if($len === 4) {
$value = strtotime("$str-01-01");
} else {
// unix timestamp
$value = (int) $str;
if(is_int($options['baseTimestamp'])) $value += $options['baseTimestamp'];
}
} else {
// i.e. '+1 DAY', '10/10/2024', 'April 8 2024'
$chars = array('-', '/', '+', '.', ' ');
foreach($chars as $c) {
if(strpos($str, $c) !== false) $is = true;
if($is) break;
}
if(!$is) $is = ctype_alnum($str); // word string with 0 space, i.e. "tomorrow"
if(!$is) return $options['emptyReturnValue'];
unset($options['outputFormat']);
$value = $this->strtotime($str, $options);
}
if($value !== $options['emptyReturnValue']) {
if($format === $defaults['outputFormat']) {
$value = date($format, $value);
} else if(empty($format) || $format === 'ts' || $format === 'U') {
// timestamp, keep as-is
} else {
$value = $this->date($format, $value);
}
}
if(empty($value)) $value = $options['emptyReturnValue'];
return (string) $value;
}
/**
@@ -1103,4 +1199,7 @@ class WireDateTime extends Wire {
}
return array();
}
public function isStrtotime($str) {
}
}

View File

@@ -1676,25 +1676,31 @@ class WireFileTools extends Wire {
// note: this fails for PHP files executable on their own (like shell scripts)
return $namespace;
}
// get everything that appears before "namespace" keyword
$head = substr($data, 0, $namespacePos);
// find where line ends after "namespace ..." keyword
foreach(array("\n", "\r", ";") as $c) {
$eol = strpos($data, $c, $namespacePos);
if($eol !== false) break;
}
// get everything that appears before "namespace", and after "namespace" on same line
$head = $eol === false ? $data : substr($data, 0, $eol);
$headPrev = $head;
// declare(...); is the one statement allowed to appear before namespace in PHP files
if(strpos($head, 'declare')) {
$head = preg_replace('/declare[ ]*\(.+?\)[ ]*;\s*/s', '', $head);
// single line comment(s) appear before namespace
if(strpos($head, '//') !== false) {
$head = preg_replace('!//[^\r\n]*!', '', $head);
}
// single line comment(s) appear before namespace
if(strpos($head, '//') !== false) {
$head = preg_replace('!//.*!', '', $head);
}
// single or multi-line comments before namespace
if(strpos($head, '/' . '*') !== false) {
$head = preg_replace('!/\*.*\*/!s', '', $head);
}
// declare(...); is the one statement allowed to appear before namespace in PHP files
if(strpos($head, 'declare')) {
$head = preg_replace('/declare[ ]*\(.+?\)[ ]*;\s*/s', '', $head);
}
// replace cleaned up head in data
if($head !== $headPrev) {

View File

@@ -47,6 +47,7 @@ class WireHooks {
* - fromClass: the name of the class containing the hooked method, if not the object where addHook was executed. Set automatically, but you may still use in some instances.
* - argMatch: array of Selectors objects where the indexed argument (n) to the hooked method must match, order to execute hook.
* - objMatch: Selectors object that the current object must match in order to execute hook
* - retMatch: Selectors object that must match the return value, or a match string to match return value
* - public: auto-assigned to true or false by addHook() as to whether the method is public or private/protected.
*
*/
@@ -58,7 +59,10 @@ class WireHooks {
'allInstances' => false,
'fromClass' => '',
'argMatch' => null,
'argMatchType' => [],
'objMatch' => null,
'retMatch' => null,
'retMatchType' => '',
);
/**
@@ -334,7 +338,7 @@ class WireHooks {
* @see WireHooks::isMethodHooked(), WireHooks::isPropertyHooked(), WireHooks::hasHook()
*
*/
public function isHooked($method, Wire $instance = null) {
public function isHooked($method, ?Wire $instance = null) {
if($instance) return $this->hasHook($instance, $method);
if(strpos($method, ':') !== false) {
$hooked = isset($this->hookClassMethodCache[$method]); // fromClass::method() or fromClass::property
@@ -631,8 +635,21 @@ class WireHooks {
$options['fromClass'] = $fromClass;
}
$retMatch = '';
$argOpen = strpos($method, '(');
if($argOpen) {
if($argOpen) {
if(strpos($method, ':(')) {
list($method, $retMatch) = explode(':(', $method, 2);
$retMatch = rtrim($retMatch, ') ');
} else if(strpos($method, ':<') && substr(trim($method), -1) === '>') {
list($method, $retMatch) = explode(':<', $method, 2);
$retMatch = "<$retMatch";
}
$argOpen = strpos($method, '(');
}
if($argOpen) {
// arguments to match may be specified in method name
$argClose = strpos($method, ')');
if($argClose === $argOpen+1) {
@@ -659,18 +676,31 @@ class WireHooks {
// just single argument specified, so argument 0 is assumed
}
if(is_string($argMatch)) $argMatch = array(0 => $argMatch);
$argMatchType = [];
foreach($argMatch as $argKey => $argVal) {
if(Selectors::stringHasSelector($argVal)) {
/** @var Selectors $selectors */
$selectors = $this->wire->wire(new Selectors());
$selectors->init($argVal);
$argMatch[$argKey] = $selectors;
}
list($argVal, $argValType) = $this->prepareArgMatch($argVal);
$argMatch[$argKey] = $argVal;
$argMatchType[$argKey] = $argValType;
}
if(count($argMatch)) {
$options['argMatch'] = $argMatch;
$options['argMatchType'] = $argMatchType;
}
if(count($argMatch)) $options['argMatch'] = $argMatch;
}
} else if(strpos($method, ':')) {
list($method, $retMatch) = explode(':', $method, 2);
}
if($retMatch) {
// match return value
if($options['before'] && !$options['after']) {
throw new WireException('You cannot match return values with “before” hooks');
}
list($retMatch, $retMatchType) = $this->prepareArgMatch($retMatch);
$options['retMatch'] = $retMatch;
$options['retMatchType'] = $retMatchType;
}
$localHooks = $object->getLocalHooks();
if($options['allInstances'] || $options['fromClass']) {
@@ -996,50 +1026,25 @@ class WireHooks {
if($type == 'method' && !empty($hook['options']['argMatch'])) {
// argument comparison to determine at runtime whether to execute the hook
$argMatches = $hook['options']['argMatch'];
$argMatchTypes = $hook['options']['argMatchType'];
$matches = true;
foreach($argMatches as $argKey => $argMatch) {
/** @var Selectors $argMatch */
$argMatchType = isset($argMatchTypes[$argKey]) ? $argMatchTypes[$argKey] : '';
$argVal = isset($arguments[$argKey]) ? $arguments[$argKey] : null;
if(is_object($argMatch)) {
// Selectors object
if(is_object($argVal)) {
$matches = $argMatch->matches($argVal);
} else {
// we don't work with non-object here
$matches = false;
}
} else if(is_string($argMatch) && strpos($argMatch, '<') === 0 && substr($argMatch, -1) === '>') {
// i.e. <Page>, <User>, <string>, <object>, <bool>, etc.
$argMatch = trim($argMatch, '<>');
if(strpos($argMatch, '|')) {
// i.e. <User|Role|Permission> or <int|float> etc.
$argMatches = explode('|', str_replace(array('<', '>'), '', $argMatch));
} else {
$argMatches = array($argMatch);
}
foreach($argMatches as $argMatchType) {
if(isset($this->argMatchTypes[$argMatchType])) {
$argMatchFunc = $this->argMatchTypes[$argMatchType];
$matches = $argMatchFunc($argVal);
} else {
$matches = wireInstanceOf($argVal, $argMatchType);
}
if($matches) break;
}
} else {
if(is_array($argVal)) {
// match any array element
$matches = in_array($argMatch, $argVal);
} else {
// exact string match
$matches = $argMatch == $argVal;
}
}
$matches = $this->conditionalArgMatch($argMatch, $argVal, $argMatchType);
if(!$matches) break;
}
if(!$matches) continue; // don't run hook
}
if($type === 'method' && $when === 'after' && !empty($hook['options']['retMatch'])) {
if(!$this->conditionalArgMatch(
$hook['options']['retMatch'],
$result['return'],
$hook['options']['retMatchType'])) continue;
}
if($this->allowPathHooks && isset($this->pathHooks[$hook['id']])) {
$allowRunPathHook = $this->allowRunPathHook($hook['id'], $arguments);
$this->removeHook($object, $hook['id']); // once only
@@ -1135,9 +1140,104 @@ class WireHooks {
}
/**
* Allow given path hook to run?
* Prepare argument match
*
* This checks if the hooks path matches the request path, allowing for both
* @param string $argMatch
* @return array
* @since 3.0.247
*
*/
protected function prepareArgMatch($argMatch) {
$argMatch = trim($argMatch, '()');
$argMatchType = '';
list($c1, $c2, $c3) = [ substr($argMatch, 0, 1), substr($argMatch, -1), substr($argMatch, 0, 2) ];
if($c1 === '<' && $c2 === '>') {
// i.e. <WireArray> or <ThisPage|ThatPage>
$argMatchType = 'instanceof';
$argMatch = trim($argMatch, '<>');
} else if($c1 === '=' || $c1 === '<' || $c1 === '>' || Selectors::isOperator($c3)) {
// selector that starts with operator and translates to "argVal matches argMatch"
$argMatch = "___val$argMatch"; // i.e. ___val=something
$argMatchType = 'selector';
}
if($argMatchType === 'instanceof') {
// ok
$argMatch = strpos($argMatch, '|') ? explode('|', $argMatch) : [ $argMatch ];
} else if(Selectors::stringHasSelector($argMatch)) {
/** @var Selectors $selectors */
$selectors = $this->wire->wire(new Selectors());
$selectors->init($argMatch);
$argMatch = $selectors;
$argMatchType = 'selector';
} else {
$argMatchType = 'equals';
}
return [ $argMatch, $argMatchType ];
}
/**
* Does given value match given match condition?
*
* @param Selectors|string $argMatch
* @param mixed $argVal
* @return bool
* @since 3.0.247
*
*/
protected function conditionalArgMatch($argMatch, $argVal, $argMatchType) {
$matches = false;
if($argMatch instanceof Selectors) {
// Selectors object
/** @var Selector $s */
$s = $argMatch->first();
if($s instanceof Selector && $s->field() === '___val') {
$o = WireData();
$o->set('value', $argVal);
$s->field = 'value';
$argVal = $o;
} else if(is_array($argVal)) {
$argVal = count($argVal) && is_string(key($argVal)) ? WireData($argVal) : WireArray($argVal);
}
if(is_object($argVal)) {
$matches = $argMatch->matches($argVal);
}
} else if($argMatchType === 'instanceof') {
if(!is_array($argMatch)) $argMatch = [ $argMatch ];
foreach($argMatch as $type) {
if(isset($this->argMatchTypes[$type])) {
$argMatchFunc = $this->argMatchTypes[$type];
$matches = $argMatchFunc($argVal);
} else {
$matches = wireInstanceOf($argVal, $type);
}
if($matches) break;
}
} else if(is_array($argVal)) {
// match any array element
$matches = in_array($argMatch, $argVal);
} else {
// exact match
$matches = $argMatch == $argVal;
}
return $matches;
}
/**
* Allow given path hook to run?
*
* This checks if the hooks path matches the request path, allowing for both
* regular and regex matches and populating parenthesized portions to arguments
* that will appear in the HookEvent.
*
@@ -1175,6 +1275,15 @@ class WireHooks {
$regexDelim = $matchPath[0];
} else {
// needs to be in regex format
if(strpos($matchPath, '.') !== false) {
// preserve some regex sequences containing periods
$r = [ '.+' => '•+', '.*' => '•*', '\\.' => '\\•' ];
$matchPath = str_replace(array_keys($r), array_values($r), $matchPath);
// force any remaining periods to be taken literally
$matchPath = str_replace('.', '\\.', $matchPath);
// restore regex sequences containing periods
$matchPath = str_replace(array_values($r), array_keys($r), $matchPath);
}
if(strpos($matchPath, '/') === 0) $matchPath = "^$matchPath";
$matchPath = "#$matchPath$#";
}

View File

@@ -76,7 +76,17 @@ class WireHttp extends Wire {
* HTTP methods we are allowed to use
*
*/
protected $allowHttpMethods = array('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH');
protected $allowHttpMethods = array(
'GET',
'POST',
'PUT',
'DELETE',
'HEAD',
'PATCH',
'OPTIONS',
'TRACE',
'CONNECT'
);
/**
* Headers to include in the request
@@ -2089,6 +2099,8 @@ class WireHttp extends Wire {
/**
* Set the number of seconds till connection times out
*
* Note that the default timeout for http requests is 4.5 seconds
*
* #pw-group-settings
*
* @param int|float $seconds

View File

@@ -3,9 +3,9 @@
/**
* ProcessWire Markup Regions
*
* Supportings finding and manipulating of markup regions in an HTML document.
* Supports finding and manipulating of markup regions in an HTML document.
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
* https://processwire.com
*
*/
@@ -37,23 +37,40 @@ class WireMarkupRegions extends Wire {
*
*/
protected $selfClosingTags = array(
'link',
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr',
'link' => 'link',
'area' => 'area',
'base' => 'base',
'br' => 'br',
'col' => 'col',
'command' => 'command',
'embed' => 'embed',
'hr' => 'hr',
'img' => 'img',
'input' => 'input',
'keygen' => 'keygen',
'link' => 'link',
'meta' => 'meta',
'param' => 'param',
'source' => 'source',
'track' => 'track',
'wbr' => 'wbr',
);
/**
* Tags that generally only appear once in the output
*
* These can be used as unnamed markup regions
*
* @var string[]
*
*/
protected $singles = array(
'html' => 'html',
'head' => 'head',
'title' => 'title',
'body' => 'body',
'main' => 'main',
'base' => 'base',
);
/**
@@ -63,14 +80,24 @@ class WireMarkupRegions extends Wire {
*
*/
protected $actions = array(
'prepend',
'append',
'before',
'after',
'replace',
'remove',
'prepend' => 'prepend',
'append' => 'append',
'before' => 'before',
'after' => 'after',
'replace' => 'replace',
'remove' => 'remove',
'update' => 'update',
);
/**
* Markup snippets that should be removed from final output
*
* @var array
* @since 3.0.250
*
*/
protected $removals = array();
/**
* Locate and return all regions of markup having the given attribute
*
@@ -737,7 +764,7 @@ class WireMarkupRegions extends Wire {
if($name && !isset($attrs[$name])) $attrs[$name] = $val;
$tag = rtrim($tag); // remove extra space we added
$tagName = strtolower($tagName);
$selfClosing = in_array($tagName, $this->selfClosingTags);
$selfClosing = isset($this->selfClosingTags[$tagName]);
$classes = isset($attrs['class']) ? explode(' ', $attrs['class']) : array();
$id = isset($attrs['id']) ? $attrs['id'] : '';
$pwid = '';
@@ -768,7 +795,7 @@ class WireMarkupRegions extends Wire {
} else {
$actionTarget = $value;
}
if($actionTarget && in_array($action, $this->actions)) {
if($actionTarget && isset($this->actions[$action])) {
// found a valid action and target
unset($attrs[$name]);
$actionType = $actionTarget === true ? 'bool' : 'attr';
@@ -786,7 +813,7 @@ class WireMarkupRegions extends Wire {
list($prefix, $action) = explode('-', $class, 2);
if(strpos($action, '-')) list($action, $actionTarget) = explode('-', $action, 2);
if($prefix && $actionTarget) {} // ignore
if(in_array($action, $this->actions)) {
if(isset($this->actions[$action])) {
// valid action, remove action from classes and class attribute
unset($classes[$key]);
$attrs['class'] = implode(' ', $classes);
@@ -804,6 +831,10 @@ class WireMarkupRegions extends Wire {
// if there's an action, but no target, the target is assumed to be the pw-id or id
if($action && (!$actionTarget || $actionTarget === true)) $actionTarget = $pwid;
if(strpos($actionTarget, '^') === 0) {
$actionType = 'tag';
}
$info = array(
'id' => $id,
'pwid' => $pwid ? $pwid : $id,
@@ -956,14 +987,38 @@ class WireMarkupRegions extends Wire {
$classes = explode(' ', $value);
$classes = array_merge($tagInfo['classes'], $classes);
$classes = array_unique($classes);
// identify remove classes
$forceAddClasses = [];
$removeMatchClasses = [];
// identify force add and remove classes
foreach($classes as $key => $class) {
if(strpos($class, '-') !== 0) continue;
$removeClass = ltrim($class, '-');
unset($classes[$key]);
while(false !== ($k = array_search($removeClass, $classes))) unset($classes[$k]);
if(strpos($class, '+') === 0){
$class = ltrim($class, '+');
$forceAddClasses[$class] = $class;
unset($classes[$key]);
} else if(strpos($class, '-') === 0) {
$removeClass = substr($class, 1);
if(strpos($removeClass, '*') !== false) $removeMatchClasses[] = $removeClass;
unset($classes[$key]);
while(false !== ($k = array_search($removeClass, $classes))) unset($classes[$k]);
}
}
$attrs['class'] = implode(' ', $classes);
if(count($classes) && count($removeMatchClasses)) {
foreach($removeMatchClasses as $removeClass) {
if(strpos($removeClass, '/') === 0) {
// already a regex
if(strrpos($removeClass, '/') === 0) $removeClass .= '/';
} else {
// convert wildcard to regex
$removeClass = '/^' . str_replace('\\*', '.+', preg_quote($removeClass, '/')) . '$/';
}
foreach($classes as $key => $class) {
if(preg_match($removeClass, $class)) {
unset($classes[$key]);
}
}
}
}
$attrs['class'] = implode(' ', array_merge($forceAddClasses, $classes));
} else {
// replace
$attrs[$name] = $value;
@@ -1050,6 +1105,17 @@ class WireMarkupRegions extends Wire {
$pos = null;
if($name === 'tag') {
if(strpos($value, '.')) {
list($tag, $class) = explode('.', $value, 2);
if(stripos($html, "<$tag") === false) return false;
$value = $class;
$name = 'class';
} else {
return stripos($html, "<$value>") || stripos($html, "<$value ");
}
}
if($value === true) {
$tests = array(
" $name ",
@@ -1095,16 +1161,21 @@ class WireMarkupRegions extends Wire {
if($name == 'id') {
$names = '(id|pw-id|data-pw-id)';
} else {
$names = preg_quote($name);
$names = preg_quote($name, '!');
}
if($value === true) {
// match only the presence of the attribute
$regex = '!<[^<>]*\s' . $names . '[=\s/>]!i';
} else if($name === 'class') {
// match class even if other class names are present
$regex = '!<[^<>]*\sclass\s*=\s*["\'][^"\'<>]*\b' . preg_quote($value) . '[\s"\']!i';
} else {
$regex = '/<[^<>]*\s' . $names . '\s*=\s*["\']?' . preg_quote($value) . '(?:["\']|[\s>])/i';
// match attribute value
$regex = '!<[^<>]*\s' . $names . '\s*=\s*["\']?' . preg_quote($value) . '(?:["\']|[\s>])!i';
}
if(preg_match($regex, $html)) $pos = true;
}
return $pos !== false;
}
@@ -1141,6 +1212,13 @@ class WireMarkupRegions extends Wire {
if(self::debug) {
$findOptions['debugNote'] = "update.$options[action]($selector)";
}
// convert to tag matching format for find() method
if(strpos($selector, '^') === 0) {
$selector = ltrim($selector, '^');
// tag is implied if in 'tag.class' format, so only add brackets if no class
if(!strpos($selector, '.')) $selector = "<$selector>";
}
$findRegions = $this->find($selector, $markup, $findOptions);
@@ -1155,10 +1233,11 @@ class WireMarkupRegions extends Wire {
if($action == 'auto') {
// auto mode delegates to the region action
$action = '';
if(in_array($region['action'], $this->actions)) $action = $region['action'];
if(isset($this->actions[$region['action']])) $action = $region['action'];
}
switch($action) {
case 'update':
case 'append':
$replacement = $region['open'] . $region['region'] . $content . $region['close'];
break;
@@ -1273,7 +1352,30 @@ class WireMarkupRegions extends Wire {
$options['action'] = 'after'; // after intended
return $this->replace($selector, '', $markup, $options);
}
/**
* Initialize given HTML for markup regions
*
* @param string $html
* @since 3.0.250
*
*/
protected function initHtml(&$html) {
$tests = [ '="<', "='<", '="&lt;', "='&lt;" ];
foreach($tests as $test) {
$apply = strpos($html, $test);
if($apply) break;
}
if($apply) {
$actions = implode('|', $this->actions);
$html = preg_replace(
'!(<[^<>]+\s(?:data-pw-|pw-)(?:' . $actions . ')=["\'])(?:<|&lt;)([^<>\'"&]+)(?:>|&gt;)(["\'])!i',
'$1^$2$3',
$html
);
}
}
/**
* Identify and populate markup regions in given HTML
*
@@ -1355,16 +1457,20 @@ class WireMarkupRegions extends Wire {
$defaults = array(
'useClassActions' => false // allow use of "pw-*" class actions? (legacy)
);
if(is_string($htmlRegions) && $recursionLevel === 1) {
$this->initHtml($htmlRegions);
}
$options = array_merge($defaults, $options);
$leftoverMarkup = '';
$hasDebugLandmark = strpos($htmlDocument, self::debugLandmark) !== false;
$debug = $hasDebugLandmark && $this->wire()->config->debug;
$debugTimer = $debug ? Debug::timer() : 0;
$this->populateSingles($htmlDocument, $htmlRegions);
if(is_array($htmlRegions)) {
$regions = $htmlRegions;
$leftoverMarkup = '';
} else if($this->hasRegions($htmlRegions)) {
$htmlRegions = $this->stripRegions('<!--', $htmlRegions);
@@ -1406,14 +1512,39 @@ class WireMarkupRegions extends Wire {
// $xregion = $region;
$action = $region['action'];
$actionTarget = $region['actionTarget'];
$actionType = $region['actionType'];
$actionTarget = ltrim($region['actionTarget']);
if(strpos($actionTarget, '.') === 0) {
$actionAttribute = 'class';
$actionTargetPrefix = '.';
} else if(strpos($actionTarget, '^') === 0) {
$actionAttribute = 'tag';
$actionTargetPrefix = '^';
} else {
$actionAttribute = 'id';
$actionTargetPrefix = '#';
}
$actionTarget = ltrim($actionTarget, '.#^');
$regionHTML = $region['region'];
$mergeAttr = $region['attrs'];
unset($mergeAttr['id']);
$documentHasTarget = $this->hasAttribute('id', $actionTarget, $htmlDocument);
$isNew = ($region['actionType'] == 'attr' && $region['action'] != 'replace');
if(!$isNew) $isNew = $action == 'before' || $action == 'after';
$documentHasTarget = $this->hasAttribute($actionAttribute, $actionTarget, $htmlDocument);
if(!$documentHasTarget) {
// if target was not matched, check for target as a single tag (html, head, body, main)
if(isset($this->singles[$actionTarget])) {
$actionTarget = "pwmr-$actionTarget";
$documentHasTarget = $this->hasAttribute('data-pw-id', $actionTarget, $htmlDocument);
}
}
if($actionType === 'attr' || $actionType === 'tag') {
$isNew = $action != 'replace' && $action != 'update';
} else {
$isNew = $action === 'before' || $action === 'after';
}
if($isNew) {
// element is newly added element not already present
@@ -1422,7 +1553,7 @@ class WireMarkupRegions extends Wire {
$attrs = $region['attrs'];
$attrStr = count($attrs) ? ' ' . $this->renderAttributes($attrs, false) : '';
if(!strlen(trim($attrStr))) $attrStr = '';
if($region['actionType'] == 'bool') {
if($actionType == 'bool') {
$regionHTML = $region['region'];
} else {
$regionHTML = str_replace($region['open'], "<$region[name]$attrStr>", $regionHTML);
@@ -1436,7 +1567,7 @@ class WireMarkupRegions extends Wire {
$pwid = empty($region['pwid']) ? $region['actionTarget'] : $region['pwid'];
$open = $region['open'];
$openLen = strlen($open);
if($openLen > 50) $open = substr($open, 0, 30) . '[sm]... +' . ($openLen - 30) . ' bytes[/sm]>';
if($openLen > 100) $open = substr($open, 0, 100) . '[sm]... +' . ($openLen - 100) . ' bytes[/sm]>';
$debugRegionStart = "[sm]" . trim(substr($region['region'], 0, 80));
$pos = strrpos($debugRegionStart, '>');
if($pos) $debugRegionStart = substr($debugRegionStart, 0, $pos+1);
@@ -1444,8 +1575,21 @@ class WireMarkupRegions extends Wire {
//$debugRegionEnd = substr($region['region'], -30);
//$pos = strpos($debugRegionEnd, '</');
//if($pos !== false) $debugRegionEnd = substr($debugRegionEnd, $pos);
$region['note'] = strtoupper($debugAction) . " [b]#{$pwid}[/b] " .
($region['actionTarget'] != $pwid ? "(target=$region[actionTarget])" : "") .
if(strpos($open, 'pw-')) {
$open = preg_replace('!\s(data-)?pw-(' . implode('|', $this->actions) . ')(=[^\s><]+)?!', '', $open);
}
if(strpos($pwid, 'pwmr-') === 0) {
$pwid = '<' . substr($pwid, 5) . '…';
$debugActionTarget = $pwid;
} else if($actionTargetPrefix === '^') {
$pwid = "<" . ltrim($pwid, '^') . '…';
$debugActionTarget = $pwid;
} else {
$pwid = $actionTargetPrefix . $pwid;
$debugActionTarget = $actionTargetPrefix . $actionTarget;
}
$region['note'] = strtoupper($debugAction) . " {$pwid} " .
($debugActionTarget != $pwid ? "(target=$debugActionTarget) " : "") .
"[sm]with[/sm] $open";
if($region['close']) {
$region['note'] .= $this->debugNoteStr($debugRegionStart) . $region['close'];
@@ -1464,7 +1608,7 @@ class WireMarkupRegions extends Wire {
} else {
// update the markup
$updates[] = array(
'actionTarget' => "#$actionTarget",
'actionTarget' => $actionTargetPrefix . $actionTarget,
'regionHTML' => $regionHTML,
'action' => $action,
'mergeAttr' => $mergeAttr,
@@ -1559,6 +1703,41 @@ class WireMarkupRegions extends Wire {
return $numUpdates;
}
/**
* Populate single-use tags as unnamed markup regions
*
* @param string $htmlDocument
* @param array|string $htmlRegions
* @since 3.0.250
*
*/
protected function populateSingles(&$htmlDocument, &$htmlRegions) {
foreach($this->singles as $tag) {
$attr = "data-pw-id=\"pwmr-$tag\"";
$find = [ "<$tag>", "<$tag " ];
$replace = [ "<$tag $attr>", "<$tag $attr " ];
$has = false;
if(is_array($htmlRegions)) {
foreach($htmlRegions as $key => $htmlRegion) {
if(stripos($htmlRegion, "<$tag") === false) continue;
$htmlRegions[$key] = str_ireplace($find, $replace, $htmlRegion);
$has = true;
}
} else if(strpos($htmlRegions, "<$tag") !== false) {
$htmlRegions = str_ireplace($find, $replace, $htmlRegions);
$has = true;
}
if($has || stripos($htmlDocument, "<$tag") !== false) {
$htmlDocument = str_ireplace($find, $replace, $htmlDocument);
$this->removals[] = " $attr";
}
}
}
/**
* Remove any <region> or <pw-region> tags present in the markup, leaving their innerHTML contents
*
@@ -1577,9 +1756,25 @@ class WireMarkupRegions extends Wire {
$updated = true;
}
if(count($this->removals)) {
$qty = 0;
$html = str_ireplace($this->removals, '', $html, $qty);
if($qty) $updated = true;
}
if(stripos($html, ' data-pw-id=') || stripos($html, ' pw-id=')) {
$html = preg_replace('/(<[^>]+)(?: data-pw-id| pw-id)=["\']?[^>\s"\']+["\']?/i', '$1', $html);
$updated = true;
$find = [];
$replace = [];
if(preg_match_all('/(<[^<>]+?)(?: data-pw-id=| pw-id=)["\']?[^>\s"\']+["\']?/i', $html, $matches)) {
foreach($matches[0] as $key => $fullMatch) {
$find[] = $fullMatch;
$replace[] = $matches[1][$key];
}
}
if(count($find)) {
$html = str_ireplace($find, $replace, $html);
$updated = true;
}
}
return $updated;
@@ -1622,7 +1817,7 @@ class WireMarkupRegions extends Wire {
protected function debugNoteStr($str, $maxLength = 0) {
$str = str_replace(array("\r", "\n", "\t"), ' ', $str);
while(strpos($str, ' ') !== false) $str= str_replace(' ', ' ', $str);
while(strpos($str, ' ') !== false) $str = str_replace(' ', ' ', $str);
if($maxLength) $str = substr($str, 0, $maxLength);
return trim($str);
}
@@ -1631,6 +1826,7 @@ class WireMarkupRegions extends Wire {
if(!count($debugNotes)) $debugNotes[] = "Nothing found";
if($debugTimer !== null) $debugNotes[] = '[sm]' . Debug::timer($debugTimer) . ' seconds[/sm]';
$out = "" . implode("\n", $debugNotes);
$out = str_replace($this->removals, '', $out);
$out = $this->wire()->sanitizer->entities($out);
$out = str_replace(array('[sm]', '[/sm]'), array('<small style="opacity:0.7">', '</small>'), $out);
$out = str_replace(array('[b]', '[/b]'), array('<strong>', '</strong>'), $out);

View File

@@ -302,6 +302,60 @@ class WireRandom extends Wire {
return $this->alphanumeric($length, $options);
}
/**
* Generate a random string using given characters
*
* @param int $length Length of string or specify 0 for random length
* @param string $characters Charaacters to use for random string or omit for partial ASCII set
* @param array $options
* - `minLength` (int): Minimum allowed length if length argument is 0 (default=10)
* - `maxLength` (int): Maximum allowed length if length argument is 0 (default=40)
* - `fast` (bool): Use a faster randomization method? (default=false)
* @return string
* @since 3.0.251
*
*/
public function string($length = 0, $characters = '', array $options = []) {
$defaults = [
'minLength' => 10,
'maxLength' => 40,
'fast' => false,
];
$options = array_merge($defaults, $options);
if(empty($characters)) {
$characters = 'abcdefghijklmnopqrstuvwxyz';
$characters .= strtoupper($characters);
$characters .= '0123456789';
$characters .= '-_;:/.,!@$%^*()-+~|';
}
if($length < 1) {
if($options['fast']) {
$length = mt_rand($options['minLength'], $options['maxLength']);
} else {
$length = $this->integer($options['minLength'], $options['maxLength']);
}
}
$str = '';
$L = strlen($characters) - 1;
for($n = 0; $n < $length; $n++) {
if($options['fast']) {
$v = mt_rand(0, $L);
} else {
$v = $this->integer(0, $L);
}
$c = substr($characters, $v, 1);
$str .= $c;
}
return $str;
}
/**
* Get a random integer
*

View File

@@ -6,7 +6,7 @@
* Wire Data Access Object, provides reusable capability for loading, saving, creating, deleting,
* and finding items of descending class-defined types.
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
* https://processwire.com
*
* @method WireArray load(WireArray $items, $selectors = null)
@@ -220,7 +220,9 @@ abstract class WireSaveableItems extends Wire implements \IteratorAggregate {
$query->execute();
$rows = $query->fetchAll(\PDO::FETCH_ASSOC);
$n = 0;
$this->loadRowsReady($rows);
foreach($rows as $row) {
if($useLazy) {
$this->lazyItems[$n] = $row;
@@ -238,6 +240,14 @@ abstract class WireSaveableItems extends Wire implements \IteratorAggregate {
return $items;
}
/**
* Called after rows loaded from DB but before populated to this instance
*
* @param array $rows
*
*/
protected function loadRowsReady(array &$rows) { }
/**
* Create a new Saveable item from a raw array ($row) and add it to $items
*
@@ -247,7 +257,7 @@ abstract class WireSaveableItems extends Wire implements \IteratorAggregate {
* @since 3.0.194
*
*/
protected function initItem(array &$row, WireArray $items = null) {
protected function initItem(array &$row, ?WireArray $items = null) {
if(!empty($row['data'])) {
if(is_string($row['data'])) $row['data'] = $this->decodeData($row['data']);
@@ -753,7 +763,7 @@ abstract class WireSaveableItems extends Wire implements \IteratorAggregate {
* @return WireLog
*
*/
public function log($str, Saveable $item = null) {
public function log($str, ?Saveable $item = null) {
$logs = $this->wire()->config->logs;
$name = $this->className(array('lowercase' => true));
if($logs && in_array($name, $logs)) {

View File

@@ -109,7 +109,7 @@ abstract class WireSaveableItemsLookup extends WireSaveableItems {
* @since 3.0.194
*
*/
protected function initItem(array &$row, WireArray $items = null) {
protected function initItem(array &$row, ?WireArray $items = null) {
$lookupField = $this->getLookupField();
$lookupValue = $row[$lookupField];

View File

@@ -90,6 +90,25 @@ class WireShutdown extends Wire {
*/
protected $error = array();
/**
* Methods that should have their arguments suppressed from PHP backtraces
*
* - Each method must include a `->`.
* - Methods should not include parenthesis.
* - If for specific class, include the class name before the `->`.
*
* @var string[]
*
*/
protected $banBacktraceMethods = array(
'->___login', // Session or ProcessLogin
'->___start', // i.e. Tfa
'->___setPass', // Password.php
'Session->___authenticate',
'Password->matches',
'Password->hash',
);
/**
* Default HTML to use for error message
*
@@ -158,7 +177,7 @@ class WireShutdown extends Wire {
E_USER_ERROR => $this->_('Error'),
E_USER_WARNING => $this->_('User Warning'),
E_USER_NOTICE => $this->_('User Notice'),
E_STRICT => $this->_('Strict Warning'),
2048 => $this->_('Strict Warning'), // 2048=E_STRICT (deprecated in PHP 8.4)
E_RECOVERABLE_ERROR => $this->_('Recoverable Fatal Error')
);
@@ -188,6 +207,7 @@ class WireShutdown extends Wire {
protected function getErrorMessage(array $error) {
$type = $error['type'];
$config = $this->config;
if(isset($this->types[$type])) {
$errorType = $this->types[$type];
@@ -203,7 +223,25 @@ class WireShutdown extends Wire {
$detail = '';
}
return "$errorType: \t$message $detail ";
$message = "$errorType: \t$message $detail ";
if(strpos($message, '#1') !== false && stripos($message, '):')) {
// backtrace likely present in $message
// methods that should have their arguments excluded from backtrace
foreach($this->banBacktraceMethods as $name) {
if(strpos($message, "$name(") === false) continue;
if(!preg_match_all('!' . $name . '\([^\n]+\)!', $message, $matches)) continue;
foreach($matches[0] as $match) {
$message = str_replace($match, '->' . $name . '(...)', $message);
}
}
}
if(strlen((string) $config->dbPass) > 4) {
$message = str_replace((string) $config->dbPass, '[...]', $message);
}
return $message;
}
/**
@@ -213,8 +251,7 @@ class WireShutdown extends Wire {
*
*/
protected function getWireInput() {
/** @var WireInput $input */
$input = $this->wire('input');
$input = $this->wire()->input;
if($input) return $input;
$input = $this->wire(new WireInput());
return $input;
@@ -228,8 +265,7 @@ class WireShutdown extends Wire {
*/
protected function getCurrentUrl() {
/** @var Page|null $page */
$page = $this->wire('page');
$page = $this->wire()->page;
$input = $this->getWireInput();
$http = isset($_SERVER['HTTP_HOST']) || isset($_SERVER['REQUEST_URI']);
@@ -552,9 +588,12 @@ class WireShutdown extends Wire {
if($useHTML && $config->ajax) $useHTML = false;
// include IP address is user name if configured to do so
if($config->logIP && $this->wire('session')) {
$ip = $this->wire('session')->getIP();
if(strlen($ip)) $name = "$name ($ip)";
if($config->logIP) {
$session = $this->wire()->session;
if($session) {
$ip = $session->getIP();
if(strlen($ip)) $name = "$name ($ip)";
}
}
// save to errors.txt log file
@@ -781,7 +820,7 @@ class WireShutdown extends Wire {
public function shutdownExternal() {
if(error_get_last()) return;
/** @var ProcessPageView $process */
$process = $this->wire('process');
$process = $this->wire()->process;
if($process == 'ProcessPageView') $process->finished();
}
}

View File

@@ -72,6 +72,11 @@ class WireTempDir extends Wire {
if(!is_null($this->tempDirRoot)) throw new WireException("Temp dir has already been created");
if(empty($name)) $name = $this->createName();
if(is_object($name)) $name = wireClassName($name, false);
if($basePath && !$this->wire()->files->allowPath($basePath, true)) {
$this->log("Given base path $basePath is not within ProcessWire assets so has been replaced");
$basePath = '';
}
$basePath = $this->classRootPath(true, $basePath);
$this->classRoot = $basePath;

View File

@@ -905,7 +905,7 @@ class WireTextTools extends Wire {
* - `has` (bool): Specify true to only return true or false if it has tags (default=false).
* - `tagOpen` (string): The required opening tag character(s), default is '{'
* - `tagClose` (string): The required closing tag character(s), default is '}'
* @return array|bool
* @return array|bool Always returns array unless you specify the `has` option as true.
* @since 3.0.126
*
*/
@@ -918,6 +918,7 @@ class WireTextTools extends Wire {
);
$options = array_merge($defaults, $options);
$str = (string) $str;
$tags = array();
$pos1 = strpos($str, $options['tagOpen']);
@@ -981,7 +982,8 @@ class WireTextTools extends Wire {
* - `tagOpen` (string): The required opening tag character(s), default is '{'
* - `tagClose` (string): The optional closing tag character(s), default is '}'
* - `recursive` (bool): If replacement value contains tags, populate those too? (default=false)
* - `removeNullTags` (bool): If a tag resolves to a NULL, remove it? If false, tag will remain. (default=true)
* - `removeNullTags` (bool): If a tag resolves to a NULL (i.e. field not present), remove it? (default=true)
* - `removeEmptyTags` (bool): If a tag value resolves to blank string, false or NULL, remove it? (default=true) 3.0.237+
* - `entityEncode` (bool): Entity encode the values pulled from $vars? (default=false)
* - `entityDecode` (bool): Entity decode the values pulled from $vars? (default=false)
* - `allowMarkup` (bool): Allow markup to appear in populated variables? (default=true)
@@ -995,7 +997,8 @@ class WireTextTools extends Wire {
'tagOpen' => '{', // opening tag (required)
'tagClose' => '}', // closing tag (optional)
'recursive' => false, // if replacement value contains tags, populate those too?
'removeNullTags' => true, // if a tag value resolves to a NULL, remove it? If false, tag will be left in tact.
'removeNullTags' => true, // If a tag resolves to a NULL (i.e. field not present on page), remove it?
'removeEmptyTags' => true, // If a tag value resolves to blank string, false or null, remove it?
'entityEncode' => false, // entity encode values pulled from $vars?
'entityDecode' => false, // entity decode values pulled from $vars?
'allowMarkup' => true, // allow markup to appear in populated variables?
@@ -1015,6 +1018,7 @@ class WireTextTools extends Wire {
if(is_object($vars)) {
if($vars instanceof Page) {
$fieldValue = $options['allowMarkup'] ? $vars->getMarkup($fieldName) : $vars->getText($fieldName);
if($fieldValue === '' && $vars->get($fieldName) === null) $fieldValue = null;
} else if($vars instanceof WireData) {
$fieldValue = $vars->get($fieldName);
} else {
@@ -1023,6 +1027,9 @@ class WireTextTools extends Wire {
} else if(is_array($vars)) {
$fieldValue = isset($vars[$fieldName]) ? $vars[$fieldName] : null;
}
// if value resolves to empty and we are not removing empty tags, then do not add to replacements
if(empty($fieldValue) && !strlen("$fieldValue") && !$options['removeEmptyTags']) continue;
// if value resolves to null and we are not removing null tags, then do not add to replacements
if($fieldValue === null && !$options['removeNullTags']) continue;

Some files were not shown because too many files have changed in this diff Show More