1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-24 15:23:11 +02:00

710 Commits

Author SHA1 Message Date
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
Ryan Cramer
2ea60ee7d5 README updates and bump version to 3.0.227 2023-09-12 10:59:21 -04:00
Ryan Cramer
41adc02373 Various minor code and phpdoc updates in several classes 2023-09-11 12:12:56 -04:00
Ryan Cramer
4bb5dbf4a6 Fix issue processwire/processwire-issues#1809 2023-09-11 10:42:43 -04:00
Ryan Cramer
013231acda Additional updates for processwire/processwire-issues#1791 admin asset file version urlsj 2023-09-11 10:21:07 -04:00
Ryan Cramer
7dc8ddc9af Add $config->versionUrls() method for versioned file URLs and update the admin themes to use it 2023-09-08 11:58:38 -04:00
Ryan Cramer
deccd2c8eb Refactor and improve $datetime->elapsedTimeStr() method to add new features and fix the plural issue. Also adds feature requested in processwire/processwire-issues#1328 2023-09-08 10:29:24 -04:00
Ryan Cramer
5b76d4e340 Fix issue processwire/processwire-issues#1808 2023-09-07 10:29:44 -04:00
Ryan Cramer
6df11e5e46 Fix issue processwire/processwire-issues#1805 2023-09-07 09:39:12 -04:00
Ryan Cramer
7ed74281ad Add suggestion from processwire/processwire-issues#1806 2023-09-07 08:35:01 -04:00
Ryan Cramer
b824f645a9 Fix issue where Lister wasn't 100% width in AdminThemeDefault or AdminThemeReno. Note that we had to disable the resizable columns in default/reno admin themes to fix it, as the current version of jquery-tablesorter-resizable doesn't seem to allow for full width tables unless you drag it to be full width. 2023-09-04 11:02:46 -04:00
Ryan Cramer
002c2d15db Additional updates for processwire/processwire-issues#1467 2023-09-01 09:35:23 -04:00
Ryan Cramer
fbf2f140a7 Fix issue processwire/processwire-issues#1804 2023-09-01 09:30:56 -04:00
Ryan Cramer
fdb5a19941 Bump version to 3.0.226 2023-08-25 14:32:50 -04:00
Ryan Cramer
571c2dd1c2 Minor CSS adjustment for InputfieldRepeater, fixes issue where wrong background color could appear on depth-enabled repeater in fieldset 2023-08-25 08:44:08 -04:00
Ryan Cramer
d61436256e Replace PHP 8.2 deprecated mb_convert_encoding() call in Sanitizer 2023-08-25 08:43:35 -04:00
Ryan Cramer
92eef72fc1 Fix issue processwire/processwire-issues#1803 2023-08-25 08:37:50 -04:00
Ryan Cramer
8292e7d8f0 Update for processwire/processwire-issues#1467 2023-08-22 11:13:02 -04:00
Ryan Cramer
359c048862 Fix issue processwire/processwire-issues#1801 2023-08-22 11:05:45 -04:00
Ryan Cramer
088cd57160 Fix issue processwire/processwire-issues#1802 2023-08-22 09:49:16 -04:00
Ryan Cramer
af80005a9c Bump version to 3.0.225 2023-08-18 15:00:22 -04:00
matjazpotocnik
a8894391a2 Fix issue processwire/processwire-issues#1571 which adds support for adding _END fieldset item with ProcessTemplateFieldCreator 2023-08-18 09:00:47 -04:00
Ryan Cramer
754fb2b1fd Add Page::matchesDatabase() method to accompany existing matches() method but query the database rather than in memory 2023-08-17 12:09:01 -04:00
Ryan Cramer
1ff6f147f8 Various minor unrelated updates 2023-08-17 11:58:59 -04:00
matjazpotocnik
efbe13ae9d Fix issue processwire/processwire-issues#1644 2023-08-17 10:57:30 -04:00
Ryan Cramer
3ce686d1e3 Update for processwire/processwire-issues#1730
Co-authored-by: matjazpotocnik <matjaz.potocnik@um.si>
2023-08-17 10:29:59 -04:00
Ryan Cramer
db88618e2d Fix issue processwire/processwire-issues#1730
Co-authored-by: matjazpotocnik <matjaz.potocnik@um.si>
2023-08-17 10:28:18 -04:00
Ryan Cramer
0efd5eec99 Fix issue processwire/processwire-issues#1462 2023-08-17 08:56:54 -04:00
Ryan Cramer
a0200795a5 Fix issue processwire/processwire-issues#901 2023-08-11 11:16:27 -04:00
Ryan Cramer
27fd0bf93e Fix issue processwire/processwire-issues#1201 2023-08-11 10:54:25 -04:00
Ryan Cramer
5a50ca01d8 Update for processwire/processwire-issues#1791 2023-08-07 09:46:53 -04:00
Ryan Cramer
0cdba2e307 Update for processwire/processwire-issues#1701 2023-08-07 09:23:32 -04:00
Ryan Cramer
7afe6fa9cf Bump version to 3.0.224 2023-08-04 14:34:36 -04:00
Ryan Cramer
0f8615bf88 phpdoc typo fix in Selectors.php 2023-08-04 11:46:57 -04:00
Ryan Cramer
044175df04 Fix issue processwire/processwire-issues#1492 2023-08-04 11:39:52 -04:00
Ryan Cramer
d6a0df74d6 Add wirePageId() and pageId() functions per processwire/processwire-issues#896 2023-08-04 10:33:50 -04:00
Ryan Cramer
13114afe08 Update Page::matches() to perform comparison with database rather than in-memory per processwire/processwire-issues#1701 2023-08-04 10:30:56 -04:00
Ryan Cramer
6266d1d86b PHP 8.1+ updates per processwire/processwire-issues#1664 2023-08-04 08:35:33 -04:00
Ryan Cramer
b7c232972e Fix issue processwire/processwire-issues#800 2023-08-03 10:58:05 -04:00
Ryan Cramer
3b5e1adb7d Update URLs for cache busting per processwire/processwire-issues#1518 2023-08-03 10:22:36 -04:00
Ryan Cramer
2f60f98992 Update to previous commit 2023-08-03 10:16:26 -04:00
Ryan Cramer
166ac1d703 Remove unused module names from wire/core/install.sql and site-blank/install/install.sql per processwire/processwire-issues#1307 2023-08-03 10:14:18 -04:00
Ryan Cramer
09300ebb96 Update PagesLoader::count() method to fully support selectors argument that is instanceof Selectors object 2023-08-02 10:14:46 -04:00
Ryan Cramer
223c0e79be Minor unrelated adjustments 2023-08-01 11:19:29 -04:00
Ryan Cramer
1271fa684a Add support for processwire/processwire-issues#1795 2023-08-01 11:15:30 -04:00
Ryan Cramer
df08140712 Update for processwire/processwire-issues#1793 2023-08-01 09:31:24 -04:00
Ryan Cramer
0245895d39 Fix issue processwire/processwire-issues#1793 2023-07-31 09:36:32 -04:00
Ryan Cramer
a2d5d62131 Fix issue processwire/processwire-issues#1792 2023-07-27 09:57:15 -04:00
Ryan Cramer
308ca91daf Fix issue processwire/processwire-issues#1762 2023-07-27 09:41:06 -04:00
Ryan Cramer
d6ca1232e2 Fix issue processwire/processwire-issues#1657 2023-07-27 09:24:40 -04:00
Ryan Cramer
b656b254e8 phpdoc update and hook example in wire/core/Users.php 2023-07-26 14:04:47 -04:00
Ryan Cramer
3dc00d6c15 Small update in InputfieldPageAutocomplete 2023-07-26 13:57:27 -04:00
Ryan Cramer
5e9a62e405 Add support for autocomplete in selecting page created user in page editor settings tab, related to processwire/processwire-issues#1318
Co-authored-by: matjazpotocnik <matjaz.potocnik@um.si>
2023-07-26 13:54:17 -04:00
Ryan Cramer
838d4363bf Fix issue processwire/processwire-issues#1788 2023-07-26 11:16:53 -04:00
Ryan Cramer
b170dfec16 Updates for processwire/processwire-issues#1664 2023-07-25 09:37:57 -04:00
Ryan Cramer
e81da24679 Fix issue processwire/processwire-issues#1518 2023-07-25 09:34:57 -04:00
Ryan Cramer
c1a939be0c Fix issue processwire/processwire-issues#1790 2023-07-24 11:37:28 -04:00
Ryan Cramer
98aa3b2dab Bump version to 3.0.223 2023-07-21 15:18:41 -04:00
Ryan Cramer
cdba83f115 Update for processwire/processwire-issues#1443 2023-07-21 11:47:10 -04:00
Ryan Cramer
4c6c2b7c89 Fix issue processwire/processwire-issues#889 2023-07-21 11:05:27 -04:00
Ryan Cramer
3b1e349a4a Add 4 new hookable methods to ProcessLogin 2023-07-21 10:06:02 -04:00
Ryan Cramer
1c70f8b5e3 Improve InputfieldPageAutoComplete so that it can use more selector features for fewer limitations on what/how you can match pages. This is to support features requested in processwire/processwire-issues#550 2023-07-21 08:51:55 -04:00
Ryan Cramer
211dd04c81 Various improvements to ProcessPageSearch module, including support for search selectors that don't pass through user input, opening more search options (see next commit with autocomplete improvements, which uses it) 2023-07-21 08:49:35 -04:00
Ryan Cramer
879b482b6a Updates to user-admin-[role] permission logic in PagePermissions.module. This (and the previous 2 commits) hopefully also fixes processwire/processwire-issues#1737 2023-07-19 14:44:45 -04:00
Ryan Cramer
05cdf08949 Update Role and User classes to use new $permissions->getDelegatedMethod() where appropriate 2023-07-19 14:29:36 -04:00
Ryan Cramer
bf144afd65 Add new methods to Permissions class ($permissions API var): getPermissionNameIds(), getDelegatedPermission(), and getDelegatedMethod() 2023-07-19 14:26:44 -04:00
Ryan Cramer
f835d8ecf9 Update for processwire/processwire-issues#1568 - clone tags with fieldset_END fields, and suppress showing fieldset_END field lists unless system or advanced mode 2023-07-19 10:14:14 -04:00
Ryan Cramer
bde089afd3 Attempt fix for issue that could cause one autoload module requiring another to load in incorrect order under certain cases 2023-07-16 10:52:44 -04:00
Ryan Cramer
407ec4b11d Various minor code updates primarily in Process modules. 2023-07-14 14:44:45 -04:00
Ryan Cramer
b77d7f98c6 Add null argument option to InputfieldForm::getErrors(null) which adds support for clearing internal errors cache 2023-07-14 14:21:05 -04:00
Ryan Cramer
67b6f5817b Fix issue processwire/processwire-issues#1784 2023-07-12 16:23:53 -04:00
Ryan Cramer
fa5fae9c58 Update for processwire/processwire-issues#1780 2023-07-12 11:27:35 -04:00
Ryan Cramer
a53e809fc4 Add support for sorting by page path or URL to PageFinder (requires PagePaths module be installed) 2023-07-10 13:39:20 -04:00
Ryan Cramer
206cd8c280 Fix issue processwire/processwire-issues#1780 2023-07-10 10:11:21 -04:00
Ryan Cramer
3b0462271d Update for processwire/processwire-issues#1757 2023-07-10 09:33:27 -04:00
Ryan Cramer
e0239d0381 Bump version to 3.0.222 2023-07-07 15:04:02 -04:00
Ryan Cramer
1d295ad04b Make ProcessPageEditLink::getFilesPage() method hookable per @Toutouwai 2023-07-07 15:03:10 -04:00
Ryan Cramer
44e5a29e0c Fix issue where files listed in ProcessLanguageTranslator were unsorted 2023-07-07 15:00:58 -04:00
Ryan Cramer
47437ced0d Fix issue processwire/processwire-issues#1403 2023-07-07 14:45:29 -04:00
Ryan Cramer
20d5016017 Minor updates in ProcessLogger, plus this should fix processwire/processwire-issues#1084 2023-07-07 14:43:35 -04:00
Ryan Cramer
203488bd4a Add support for conditional hooks that match by argument type. More details here: https://processwire.com/docs/modules/hooks/#conditional-hooks-by-type 2023-07-07 11:37:52 -04:00
Ryan Cramer
52ac627506 Fix issue in PageFrontEdit.js where trigger() was being called on non-jQuery element, converted to JS click() 2023-07-06 12:32:37 -04:00
Ryan Cramer
2759109de5 Fix issue processwire/processwire-issues#1777 2023-07-05 11:35:03 -04:00
Ryan Cramer
530a2c4ecd WireHttp upgrades and fixes from processwire/processwire-issues#1090 - the main upgrade is adding support for buffered download to the downloadFopen method. Also fixes cleanup issue when download method fails and a backup method takes over 2023-07-03 10:38:36 -04:00
Ryan Cramer
3f184f9c14 Add support for multi-language translatable months and days (full and abbreviated) to WireDateTime class. This is necessary in PHP 8.1+ since PHP dropped the strftime() function. This is also related to the request in processwire/processwire-issues#1774 2023-07-03 09:28:24 -04:00
Ryan Cramer
5169693f57 Fix issue processwire/processwire-issues#1776 2023-07-03 08:30:13 -04:00
Ryan Cramer
91184cefb3 Fix issue processwire/processwire-issues#1773 2023-06-30 10:10:48 -04:00
Ryan Cramer
834ddfba75 Add delete(), patch() and put() methods to WireHttp 2023-06-30 09:59:46 -04:00
Ryan Cramer
2349599aa8 Fix issue processwire/processwire-issues#1770 2023-06-29 15:10:43 -04:00
Ryan Cramer
19be4b3b90 Fix issue processwire/processwire-issues#1771 2023-06-29 13:56:55 -04:00
Ryan Cramer
6339410283 Fix issue processwire/processwire-issues#1769 2023-06-28 09:26:25 -04:00
Ryan Cramer
1749ba17ac Fix issue processwire/processwire-issues#1767 2023-06-28 09:14:22 -04:00
Ryan Cramer
c8e9b6c65a Fix issue processwire/processwire-issues#1766 2023-06-26 10:33:56 -04:00
Ryan Cramer
c049dd4f58 Fix issue processwire/processwire-issues#1768 2023-06-26 10:11:30 -04:00
Ryan Cramer
1830bf124b Fix issue in ProcessPageLister/ProcessPageListerPro where it wasn't allowing header click-to-sort on all columns where it was supported 2023-06-25 09:10:26 -04:00
Ryan Cramer
3fa65dfc83 Bump version to 3.0.221 2023-06-23 13:49:33 -04:00
Ryan Cramer
18d2fdf94a Update ProcessPageEditImageSelect to use proper HTML class name attributes for checkbox and radio buttons when used with AdminThemeUikit. Plus other minor code improvements 2023-06-23 13:47:34 -04:00
Ryan Cramer
d6ba21fcaf Attempt fix for processwire/processwire-issues#1760 2023-06-23 12:43:23 -04:00
Ryan Cramer
3e98166a2a Attempt fix for issue processwire/processwire-issues#1756 2023-06-23 11:32:25 -04:00
Ryan Cramer
a6795b61ce Add feature request processwire/processwire-issues#1757 which adds support for non-plural translations of 0 quantities in the _n('%d item', %d items', 0); translation function. To use, translate the file wire/modules/LanguageSupport/LanguageTranslator.php and edi the "Is zero (0) plural or singular?" setting at the top. 2023-06-23 11:03:44 -04:00
Ryan Cramer
a01b922efb Add support for InputfieldSelect and InputfieldRadios that can be specified in language translation comment, i.e. __('Red'); // Color to use for this language? options=[Red,Blue,Green] type=radios 2023-06-23 10:55:14 -04:00
Ryan Cramer
73ab10658c Fix issue processwire/processwire-issues#1761 2023-06-22 12:44:04 -04:00
Ryan Cramer
ead96474c6 Fix issue processwire/processwire-issues#1763 2023-06-22 11:40:23 -04:00
Ryan Cramer
d089e07a63 Attempt fix for processwire/processwire-issues#1764 2023-06-22 11:31:35 -04:00
Ryan Cramer
fc5b67508f Fix issue processwire/processwire-issues#1765 2023-06-22 10:44:41 -04:00
Ryan Cramer
f1c20282f6 Minor code updates in ProcessPageAdd 2023-06-16 12:16:24 -04:00
Ryan Cramer
9353a8ea43 Add PR #273 Make error messages translatable in ProcessPageAdd
Co-authored-by: jmartsch <jmartsch@gmail.com>
2023-06-16 11:59:11 -04:00
Ryan Cramer
3fd4073f34 Minor code improvements to PageTable, plus add PR #266
Co-authored-by: jmartsch <jmartsch@gmail.com>
2023-06-16 11:45:58 -04:00
Ryan Cramer
fde9c1c2e2 Fix issue processwire/processwire-issues#1755 2023-06-16 09:02:23 -04:00
Ryan Cramer
55ec568070 Fix issue processwire/processwire-issues#1753
Co-authored-by: matjazpotocnik <matjaz.potocnik@um.si>
2023-06-15 09:11:29 -04:00
Ryan Cramer
0040345932 Fix issue processwire/processwire-issues#1751 2023-06-13 11:58:16 -04:00
Ryan Cramer
c8c5b46e77 Fix issue ryancramerdesign/InputfieldTinyMCE#20 TinyMCE when used with blank object properties in custom JSON settings 2023-06-13 11:39:36 -04:00
Ryan Cramer
dfcbb10f9d Bump version to 3.0.220 2023-06-09 15:28:28 -04:00
Ryan Cramer
b8da82d5c2 Fix issue processwire/processwire-issues#1738 2023-06-09 10:56:48 -04:00
Ryan Cramer
e59a44a83d Fix issue processwire/processwire-issues#1743 2023-06-08 16:42:34 -04:00
Ryan Cramer
16d0d77d4c Fix issue processwire/processwire-issues#1617 using suggestion from @BitPoet
Co-authored-by: BitPoet <BitPoet@users.noreply.github.com>
2023-06-08 16:31:52 -04:00
Ryan Cramer
2d790b6c64 Small fix to hookable saveModuleConfigData() in Modules.php 2023-06-08 15:27:45 -04:00
Ryan Cramer
6fe5c10dde Fix issue processwire/processwire-issues#1744 2023-06-07 12:47:01 -04:00
Ryan Cramer
bc3cd4615e Fix issue processwire/processwire-issues#1750 2023-06-07 12:40:28 -04:00
Ryan Cramer
3f14a29ff3 Fix issue processwire/processwire-issues#1664 ModulePlaceholder class mentioned by @designconcepts 2023-06-06 12:03:10 -04:00
Ryan Cramer
8129d4a6e1 Fix issue processwire/processwire-issues#1742 2023-06-06 09:43:37 -04:00
Ryan Cramer
176bd8eeac Minor updates in various classes 2023-06-05 13:35:04 -04:00
Ryan Cramer
aef78c9ca4 Fix issue processwire/processwire-issues#1747 2023-06-05 13:31:02 -04:00
Ryan Cramer
ac0239e5be Fix issue processwire/processwire-issues#1748 2023-06-05 13:25:45 -04:00
Ryan Cramer
50db3684fb Minor code updates in InputfieldSelector 2023-06-03 11:01:14 -04:00
Ryan Cramer
3b5bad2c58 Update in Modules to correct fatal error when upgrading from a much older version of ProcessWire 2023-06-03 10:31:39 -04:00
Ryan Cramer
0503a1bf68 Fix typo in Modules.php 2023-06-02 18:34:06 -04:00
Ryan Cramer
5f4da6e8f8 Bump version to 3.0.219 2023-06-02 14:55:00 -04:00
Ryan Cramer
3afdc531e0 Fix issue processwire/processwire-issues#1745 2023-06-02 14:54:21 -04:00
Ryan Cramer
e2179a6ec1 Major overhaul of Modules class, splitting it up into 6 other more focused classes 2023-06-02 14:53:12 -04:00
Ryan Cramer
02e3e0cd1b Add support for a WireCacheInterface::maintenance($obj) method as mentioned by @BitPoet 2023-05-30 15:28:03 -04:00
Ryan Cramer
f3a0b265ee Fix issue processwire/processwire-issues#1741 2023-05-30 13:45:00 -04:00
Ryan Cramer
8046f1989b Additional performance optimizations to Modules class, plus fix issue with some moduleInfo properties falling back to default value when they weren't supposed to 2023-05-30 12:02:16 -04:00
Ryan Cramer
caed81876e Bump version to 3.0.218 2023-05-26 13:50:45 -04:00
Ryan Cramer
ce06ffa496 Additional improvements to WireCache, WireCacheInterface and WireCacheDatabase 2023-05-26 13:35:30 -04:00
Ryan Cramer
0747c54ddf Update Modules class to use its own internal caches rather than using those from $cache API var. Now it only uses $cache as a backup. This means you can now safely omit the 'caches' table from DB exports, or you delete the caches table, and it'll get re-created etc. After upgrading to this version, you'll have to do a 'Modules > Refresh' before it'll start using its own caches. 2023-05-26 13:29:02 -04:00
Ryan Cramer
797d6c9b37 Fix typo in Modules.php that makes moduleInfo[versionStr] return 0.0.0 rather than correct version string 2023-05-22 16:06:05 -04:00
Ryan Cramer
d455ab71cb Add $pages->loader()->findCache() method - Find pages and cache the result for specified period of time. This is an experimental method that may find it's way to the public $pages API but is currently behind $pages->loader() while we experiment with it more and make sure it's worthwhile. 2023-05-19 14:54:04 -04:00
Ryan Cramer
481f6bbbdb Various improvements to the Modules class for improved module loading and boot time performance 2023-05-19 11:18:55 -04:00
Ryan Cramer
6981e3009e Major refactor of WireCache which now isolates the cache getting/saving/deleting to a separate module/class implementing the WireCacheInterface interface. Eventually this will enable one to modify/replace where and how PW's cache data is stored. For instance, file system, Redis, Memcache, etc. The default class is WireCacheDatabase which stores cache data in the database, as WireCache did prior to this update. 2023-05-19 11:12:49 -04:00
Ryan Cramer
a76e54193e Fix issue processwire/processwire-issues#1735 2023-05-18 11:50:29 -04:00
Ryan Cramer
3598fb113b Fix issue processwire/processwire-issues#1732 2023-05-18 09:41:08 -04:00
Ryan Cramer
47086751d2 Bump version to 3.0.217 2023-05-05 13:00:14 -04:00
Ryan Cramer
013f9ebade Add an additional check to WireDatabaseBackup so that a non-readable DB file doesn't cause a fatal exception 2023-05-05 08:53:15 -04:00
Ryan Cramer
6661f0490a Update ProcessPageEditLink by adding a ___buildForm() method so that one can manipulate what the "edit link" form shows by hooking that new method 2023-05-05 08:51:44 -04:00
Ryan Cramer
daeb6d4087 Fix jQuery 3.x issue where modal dialog buttons weren't displaying when launched from asmSelect edit links 2023-05-05 08:50:13 -04:00
Ryan Cramer
3b2c456c1a Fix issue processwire/processwire-issues#1729 2023-05-05 08:45:32 -04:00
Ryan Cramer
f2a72ad8ea Fix issue processwire/processwire-issues#1716 2023-05-03 14:05:58 -04:00
Ryan Cramer
a9b656c45e Minor adjustments in MarkupFieldtype class 2023-05-03 11:07:00 -04:00
Ryan Cramer
9bfce99352 Fix issue processwire/processwire-issues#1719 2023-05-03 11:03:34 -04:00
Ryan Cramer
f6c2ad6ed5 Fix issue processwire/processwire-issues#1726 2023-05-03 10:31:10 -04:00
Ryan Cramer
3658efd3af Fix issue processwire/processwire-issues#1727 2023-05-03 10:21:55 -04:00
netcarver
580641d69f Add PR #270 - fix missing apostrophe in a phpdoc section of WireInput 2023-05-02 10:06:58 -04:00
Ryan Cramer
d832982228 Add PR #269 for support of overriding browser title in AdminThemeFramework class
Co-authored-by: BernhardBaumrock <office@baumrock.com>
2023-05-02 10:02:08 -04:00
Ryan Cramer
908479d786 Fix issue where Inputfield showIf condition that matches a single period "." was not working correctly due to jQuery adjustments from a couple weeks ago 2023-05-02 09:08:59 -04:00
Ryan Cramer
2fcb2f4bda Fix issue in Lister where displaying File (single) > Count displayed blank rather than 0 or 1. 2023-05-01 08:30:33 -04:00
Ryan Cramer
28eb798c9c Fix issue processwire/processwire-issues#1720 2023-04-25 10:46:06 -04:00
Ryan Cramer
665a462501 Bump version to 3.0.216 plus some other minor updates 2023-04-21 14:48:00 -04:00
Ryan Cramer
0d2346baab Update Vex version to to 4.1.0 2023-04-21 10:50:31 -04:00
Ryan Cramer
749d64a756 Upgrade jQuery Magnific popup version to 1.1.0 and modify for jQuery 3.x deprecations
Co-authored-by: matjazpotocnik <matjaz.potocnik@um.si>
2023-04-21 10:32:45 -04:00
Ryan Cramer
3052f0c77d Update jQuery UI timepicker addon to latest version
Co-authored-by: matjazpotocnik <matjaz.potocnik@um.si>
2023-04-21 09:33:53 -04:00
Ryan Cramer
3d6278e23d Additional .js file updates for jQuery 3.6 2023-04-21 09:16:12 -04:00
Ryan Cramer
3fc952f674 Other minor unrelated updates 2023-04-20 12:24:40 -04:00
Ryan Cramer
3430610092 Add new $modules->getModuleInstallUrl('ModuleName'); method that returns the URL to install given module name 2023-04-20 11:02:40 -04:00
Ryan Cramer
440f649e39 Minor updates to site-blank profile, primarily to replace default rich-text editor with TinyMCE for new site-blank installs 2023-04-20 11:01:51 -04:00
Ryan Cramer
e307eab3f3 Add InputfieldTinyMCE to core 2023-04-20 10:31:59 -04:00
Ryan Cramer
844a45ff2f Upgrade jQuery TableSorter version to 2.31.3
Co-authored-by: matjazpotocnik <matjaz.potocnik@um.si>
2023-04-19 10:11:34 -04:00
Ryan Cramer
1d7f029fa5 Update many other .js files for jQuery 3.x deprecations 2023-04-19 10:08:54 -04:00
Ryan Cramer
3f2488de7d Fix inputfields.js JS issue mentioned in forum via @matjazpotocnik
Co-authored-by: matjazpotocnik <matjaz.potocnik@um.si>
2023-04-16 09:58:51 -04:00
Ryan Cramer
318bc0aba3 Bump version to 3.0.215 2023-04-14 15:54:35 -04:00
Ryan Cramer
1e60c69b97 Additional .js file updates for newer jQuery 2023-04-14 15:15:22 -04:00
Ryan Cramer
bd0253477e Update lots of core .js files for jQuery 3.6.4 support 2023-04-14 11:54:28 -04:00
Ryan Cramer
f11843562a Add support for latest version of jQuery UI (v1.13.2). This is used when $config->debug = 'dev'; in /site/config.php. Expect occasional JS/output issues when using this latest version of jQuery UI. We'll be upgrading the default core version to this over a period of time, which is why it's only used if $config->debug=='dev'; at present. 2023-04-14 09:25:25 -04:00
Ryan Cramer
885e94733a Update core jQuery versions to 1.12.4 and 3.6.4 (dev/latest). Note the dev version is used in the admin when $config->debug='dev'; 2023-04-13 09:49:21 -04:00
Ryan Cramer
0d18728523 Update InputfieldPage to support dynamic page properties in selector when used without FieldtypePage 2023-04-10 10:04:22 -04:00
Ryan Cramer
f4a05789f1 Attempt fix for issue #1714, plus a couple other minor unrelated class code improvements that were already in the queue 2023-04-07 09:34:18 -04:00
Ryan Cramer
de8307a2a9 Add method to MarkupAdminDataTable for specifying that a particular column is not sortable in an otherwise sortable table 2023-03-31 11:08:35 -04:00
Ryan Cramer
4f75a7e81c Minor code updates for various core classes 2023-03-31 11:06:33 -04:00
Ryan Cramer
57a9d224de Minor code improvements in ProcessPageList module files 2023-03-31 10:18:54 -04:00
Ryan Cramer
dad712ebb2 Updated PaginatedArray::getTotal() method to return the count() quantity when no total has been set (previously it returned 0 if not set) 2023-03-31 09:42:26 -04:00
Ryan Cramer
134edd9445 Fix issue processwire/processwire-issues#1713 2023-03-31 09:05:32 -04:00
Ryan Cramer
7352d1d7f3 Fix issue processwire/processwire-issues#1712 2023-03-30 09:28:24 -04:00
Ryan Cramer
371ea35036 Fix issue processwire/processwire-issues#1711 2023-03-30 08:46:17 -04:00
Ryan Cramer
5c23e85a79 Fix issue processwire/processwire-issues#1705
Co-authored-by: matjazpotocnik <matjaz.potocnik@um.si>
2023-03-30 07:47:35 -04:00
Ryan Cramer
b7bec30fb7 Minor adjustment to preparation of table + column helper in WireDatabasePDO 2023-03-24 13:12:56 -04:00
Ryan Cramer
43aa000f02 Fix issue processwire/processwire-issues#1709 2023-03-24 11:33:10 -04:00
Ryan Cramer
16be65c454 Correct issue with all logs returning same mtime and size in ProcessLogger 2023-03-24 10:59:22 -04:00
Ryan Cramer
f7ee00d785 Fix issue with WireLog::getLogs() returning regular PHP array rather than associative array indexed by log name 2023-03-22 14:29:59 -04:00
Ryan Cramer
547fb4cf71 Bump version to 3.0.214 2023-03-17 14:46:13 -04:00
Ryan Cramer
504dd173a1 Minor code improvements to MarkupQA class 2023-03-17 14:43:43 -04:00
Ryan Cramer
e36742528f Fix issue processwire/processwire-issues#1703 PageFrontEdit and InputfieldTinyMCE 2023-03-17 12:15:51 -04:00
Ryan Cramer
3af565ccc3 Add a new $files->size($path) method that returns the size (bytes) of the given $path (file or directory). When given a directory, it returns the combined size of all files in the directory, recursively. 2023-03-17 09:41:05 -04:00
Ryan Cramer
b41fc7feff Minor improvements to WireDatabaseBackup class 2023-03-17 09:40:28 -04:00
Ryan Cramer
4960d8f891 Improve documentation for $session->close(); method, explaining in detail when you might use the method. 2023-03-17 09:39:08 -04:00
Ryan Cramer
f2a1ebb7b4 Various improvements to FileLog and WireLog classes. Plus add new deleteAll() and pruneAll() methods to WireLog class. 2023-03-17 09:37:33 -04:00
Ryan Cramer
7021347dec Move logic for wireBytesStr() function into WireNumberTools class bytesToStr() method, while also making improvements to the method. Also add a strTobytes() method that does the opposite of bytesToStr. This also incorporates the addition of terabytes support submitted in another PR from @matjazpotocnik. 2023-03-10 13:03:30 -05:00
Ryan Cramer
42e56c5f3e Add PR #260 which makes improvements to MarkupPagerNav pagination
Co-authored-by: matjazpotocnik <matjaz.potocnik@um.si>
2023-03-09 11:14:53 -05:00
netcarver
edcfa915d8 Add PR #262 - Fix potentially missed else conditionals 2023-03-09 10:26:06 -05:00
chriswthomson
dafceffc6f Add PR #261 - Added page-edit-redirects optional permission 2023-03-09 10:17:41 -05:00
Ryan Cramer
fb980f24c7 Additional update to previous commit that prevents field_repeater.count=0 from matching any pages that don't have field_repeater, even if template not specified in the selector. 2023-03-09 09:21:54 -05:00
Ryan Cramer
4f00c1f6f1 Add pages.find selector support for matching repeaters with no row present in field_repeater table. Examples: repeater_field.count=0, repeater_field.count<2, repeater_field.count!=1, repeater_field.count>=0, etc. Previously these would only match if the page having the repeater field had been saved at least once since the repeater field was added (and thus it had a DB row to match). Now it no longer needs a DB row present to match 0 count. Related to processwire/processwire-issues#1701 2023-03-09 09:02:14 -05:00
Ryan Cramer
09163d2a76 Attempt fix issue processwire/processwire-issues#1689 2023-03-08 11:40:09 -05:00
Ryan Cramer
1c8ac461f2 Fix issue processwire/processwire-issues#1697 2023-03-08 11:02:01 -05:00
Ryan Cramer
bd6fe70347 Fix issue processwire/processwire-issues#1699 2023-03-08 10:22:30 -05:00
Ryan Cramer
592a443bf1 Fix issue in InputfieldSelect where non-default language label for optional "Please select" label was not translated when options are added using a newline-separated string. 2023-03-08 07:59:02 -05:00
Ryan Cramer
d2cde11e4d Bump version to 3.0.213 2023-02-24 14:06:06 -05:00
Ryan Cramer
2b47a39950 Add a new WireNumberTools core class. More will be added later, but this class starts with one method for unique number/ID generation. This is useful for generating unique IDs for things that may not already have them, and ensuring that the ID remains unique for the lifetime of the installation. 2023-02-24 10:23:23 -05:00
Ryan Cramer
80f700096a Add support for Fieldtype::getFieldSetups() to ProcessField. Also add some additional grouping logic to the field Type selection when creating new fields. In addition to the new optgroups shown, it also shows an optgroup for uninstalled Fieldtypes. 2023-02-24 10:20:22 -05:00
Ryan Cramer
5855c8c8b6 Implement new getFieldSetups() method in these Fieldtypes: Datetime, File, Image, Options, Page, and Textarea. 2023-02-24 10:19:32 -05:00
Ryan Cramer
b3913a8791 Add a new hookable Fieldtype::___getFieldSetups() that lets any Fieldtype specify different configurations available when creating new fields. These configurations will be selectable when creating a new field in the admin, or when setting the $field->type = 'FieldtypeName.setupName"; 2023-02-24 10:15:05 -05:00
Ryan Cramer
91f4b7cd6e Bump version to 3.0.212 2023-02-17 10:43:54 -05:00
Ryan Cramer
5a4ac84301 Additional updates for ProcessPageEditLink module 2023-02-17 09:45:52 -05:00
Ryan Cramer
7fa8fd21f0 Add new uploadName() method/property to Pagefile/Pageimage that returns original unsanitized uploaded filename. This works only for files uploaded following this commit, since we didn't previously record the info. This is to answer the feature request in processwire/processwire-requests#56 and this uploadName idea was suggested by @BernhardBaumrock 2023-02-17 08:52:52 -05:00
Ryan Cramer
a716232172 Add feature request processwire/processwire-requests#480 to support other file extensions for translatable files in ProcessLanguageTranslator.module as a module config setting 2023-02-16 09:21:53 -05:00
Ryan Cramer
b155596089 Add 2 new methods to $sanitizer: htmlClass() and htmlClasses(), for sanitizing HTML class attribute values. 2023-02-15 10:26:11 -05:00
Ryan Cramer
a29da160af Upgrades to ProcessPageEditLink with most notable addition being support for using link classes specified in the TinyMCE/CKEditor rather having to also add them to the ProcessPageEditLink module configuration 2023-02-15 10:22:50 -05:00
Ryan Cramer
d272fc9b09 Updates to InputfieldEmail that were supposed to be in the previous commit 2023-02-15 08:33:59 -05:00
Ryan Cramer
1a633a74ae Add support for IDN email and UTF-8 local-part emails to InputfieldEmail per processwire/processwire-issues#1680 and PR #259
Co-authored-by: poljpocket <poljpocket@gmail.com>
2023-02-14 11:52:20 -05:00
Ryan Cramer
104c1cddbe Add feature request processwire/processwire-requests#479 2023-02-14 11:29:57 -05:00
Ryan Cramer
c3b9c72df9 Additional updates for processwire/processwire-issues#1467 2023-02-14 09:28:03 -05:00
Ryan Cramer
ff1ba95e37 Fix issue processwire/processwire-issues#1687 2023-02-14 09:00:21 -05:00
Ryan Cramer
a1a72e5ca3 Fix issue processwire/processwire-issues#1684 2023-02-14 08:57:09 -05:00
Ryan Cramer
0f6cd3c148 Add a session cache for column options, optimization in ProcessPageLister 2023-02-09 13:55:23 -05:00
Ryan Cramer
792eff6d41 Fix issue processwire/processwire-issues#1683 2023-02-09 11:56:29 -05:00
Ryan Cramer
b289cb03aa Update FieldtypeText to automatically add "HTML Entity Encoder" textformatter to newly created text fields (and descending types). Also update it to add warning when editing an existing field and it allows HTML in formatted output, and we aren't sure that's intended. Lastly, update FieldtypeTextarea to identify when the HTML Entity Encoder has been added to a field where HTML is clearly intended and warn the user that they should remove that from the Text formatters. (This is necessary since it may have been automatically added when the field was created.) 2023-02-09 10:49:06 -05:00
Ryan Cramer
1171241f5d Add new Fieldtype::saveFieldReady(Field $field) hook that is called right before a Field object is about to be saved. For newly created fields, the given $field will have $field->id==0 2023-02-09 09:54:28 -05:00
Ryan Cramer
e86eb7fcf8 Code improvements to ProcessPageEdit link module, plus add feature request processwire/processwire-requests#477 2023-02-03 14:00:31 -05:00
Ryan Cramer
b1313438ea Add PR #257 which replaces deprecated utf8_encode() with mb_convert_encoding() in PWPNG.php
Co-authored-by: jnessier <jnessier@users.noreply.github.com>
2023-02-03 10:13:34 -05:00
Ryan Cramer
a3fa73aec6 A few phpdoc updates in config.php 2023-02-03 09:29:04 -05:00
Ryan Cramer
0e709b148c Fix issue processwire/processwire-issues#1677 while also optimizing the debug mode detection code in ProcessWire.php 2023-02-03 09:27:11 -05:00
Ryan Cramer
36bb44e0a5 Fix issue processwire/processwire-issues#1675 2023-02-03 08:15:43 -05:00
Ryan Cramer
83dfad0199 Fix issue processwire/processwire-issues#1673 2023-02-03 08:07:21 -05:00
Ryan Cramer
d5c59e7f10 Fix issue processwire/processwire-issues#1678 2023-02-03 07:48:32 -05:00
Ryan Cramer
22250b483f Update so that requests containing a double slash at the end of the path get redirected to the single slash (or no-slash) version 2023-02-02 14:21:53 -05:00
Ryan Cramer
dd93de3f91 Add getAccessTemplate() method to RepeaterPage class to fix issue where some repeater permissions did not always inherit from the page that owns it, resulting in a case where files in repeater using 'secureFiles' option could be blocked when they should have been available. 2023-02-02 14:19:53 -05:00
Ryan Cramer
b74f6ca359 Attempt fix for processwire/processwire-issues#1459 and processwire/processwire-issues#1297 by rewriting code that builds pages_parents table and requires fewer changes to the table. This is called on page parent changes and clone operations. Needs further testing on installation with 1+ million pages to compare with previous and confirm performance improvement while maintaining same accuracy. 2023-02-02 13:54:51 -05:00
Ryan Cramer
7997a40e21 Minor code improvements 2023-01-27 15:41:30 -05:00
JanRomero
15e8a4d0e6 Add PR #236 - Fix sanitizer()->date() outputting NULL for valid but falsey values 2023-01-27 15:37:53 -05:00
Ryan Cramer
3e81b0fc4d Hookable upgrades to InputfieldImage, including the ability to add thumbnail icon actions, the ability to add new buttons in image-edit mode, and full working examples in the phpdoc, plus add an EXIF working example for the image actions dropdown. This originated from PR #251 which added a Download button. But I decided we didn't really need a download button (since you can right click on image in image-edit mode and "Save as...") and instead took the additions from the PR and turned it into a hookable feature so you can add any needed actions relatively easily, whether download or something else.
Co-authored-by: JanRomero <JanRomero@users.noreply.github.com>
2023-01-27 10:58:20 -05:00
Ryan Cramer
474e31b2be Fix issue processwire/processwire-issues#1671 2023-01-26 10:15:34 -05:00
Ryan Cramer
47f1e8a089 Fix issue processwire/processwire-issues#1670 plus minor optimizations to template importing 2023-01-26 09:55:19 -05:00
Ryan Cramer
2ebb0055be Fix issue processwire/processwire-issues#1669 2023-01-26 09:06:33 -05:00
Ryan Cramer
3094bd952a Fix issue with protected files not being available to user with access when file was in repeater and page owning repeater had no template file. 2023-01-26 08:01:41 -05:00
Ryan Cramer
bc2749b76c Bump version to 3.0.211 2023-01-20 15:52:21 -05:00
Ryan Cramer
cc99fc3c92 Minor optimizations to PagePermissions module 2023-01-20 09:22:49 -05:00
Ryan Cramer
df81fdfd0b Minor optimizations and improvements to WireUpload class 2023-01-20 09:21:29 -05:00
Ryan Cramer
a435e9291c Fix issue where sometimes language tabs were pre-selecting the wrong language, or showing a selected tab that didn't match the input language. This might also be related to and fix processwire/processwire-issues#1411 (?) 2023-01-20 09:18:50 -05:00
Ryan Cramer
0b79e38d9d Fix issue processwire/processwire-issues#1118 fixes to multi-language textarea and file/image fields when template has 'noLang' option set and user is editing page in non-default language 2023-01-20 09:10:33 -05:00
hiboudev
e34a190eeb Add PR #223 WireMail: Use 'fromName' from config 2023-01-19 13:10:21 -05:00
FlipZoomMedia
e0af32189e Add PR #224 which adds a config option to ProcessPageEditLink to disable the link text editing feature, thereby enabling support for links containing existing markup 2023-01-19 11:59:29 -05:00
FlipZoomMedia
47d7aabe28 Add PR #225 to convert some count() calls to wireCount() calls in ProcessPageEditImageSelect 2023-01-19 11:30:56 -05:00
pine3ree
4d27f2a9ea Add PR #229 to fix processwire/processwire-issues#1586 sub-issue 1 in InputfieldFile file replacement logic 2023-01-19 11:21:14 -05:00
pine3ree
a5869294fb Add PR #231 simpler arrayToCSV builder in MarkupPagerNav 2023-01-19 11:13:08 -05:00
Ryan Cramer
5fb52e9d1c Add suggestion from PR #237 to support custom rows definition for translatable fields and have it use that when the user is editing the translation text. To use, specify "rows=3" somewhere in the translation comment, i.e. __('text'); // rows=3. Also added support for type=name in the comment, that lets you specify what Inputfield type to use. To use, replace the name part with "text", "textarea", "email", etc. Or if you prefer you can use the full Inputfield name, i.e. InputfieldText, InputfieldTextarea, etc.
Co-authored-by: pine3ree <pine3ree@gmail.com>
2023-01-19 10:47:09 -05:00
pine3ree
4c71204073 Add PR #238 allow custom precision float to be returned from Pageimage::ratio() 2023-01-19 10:19:27 -05:00
pine3ree
a5a2532a7d Add PR #239 fix typo in /wire/config.php for sessionAllow property 2023-01-19 10:12:40 -05:00
Ryan Cramer
b1d735170f Add updates to MarkupRSS similar to those suggested in PR #252 which allows for the option of item descriptions to contain HTML. Applies only if itemDescriptionLength=0 2023-01-19 09:16:18 -05:00
Ryan Cramer
6e4cfb9d03 Combined two "opened" event handlers in InputfieldImage.js into one 2023-01-19 07:45:04 -05:00
Ryan Cramer
45c85311b3 Optimization to prevent overhead when PagesRaw.find() matches no pages 2023-01-19 07:43:02 -05:00
Ryan Cramer
2c65cbaa5b Fix issue processwire/processwire-issues#1465 for "InputfieldImage proportional thumbnails not displayed properly in some situations" by adding suggested fix by @Toutouwai 2023-01-13 10:06:34 -05:00
Ryan Cramer
a0b91e4472 Upgrade htmlpurifier to 4.15.0, should also resolve processwire/processwire-issues#1664 2023-01-13 08:54:36 -05:00
Ryan Cramer
11a16d5693 Fix issue processwire/processwire-issues#1663 2023-01-13 08:37:20 -05:00
Ryan Cramer
b12c7e9031 Refactor repeater.js to use data-name instead of ID as needed for module by @BernhardBaumrock and PR #253
Co-authored-by: BernhardBaumrock <office@baumrock.com>
2023-01-13 08:24:52 -05:00
Ryan Cramer
6ff498f503 Happy New Year 2023-01-06 13:28:52 -05:00
Ryan Cramer
cc43f8e676 Bump version to 3.0.210, plus some minor documentation updates 2023-01-06 13:22:30 -05:00
Ryan Cramer
d45a6be4df Fix issue processwire/processwire-issues#1642 2023-01-06 08:50:10 -05:00
Ryan Cramer
834f3507d5 Fix issue processwire/processwire-issues#1662 2023-01-06 07:56:27 -05:00
Ryan Cramer
e7edfad27a Update for processwire/processwire-issues#1651 2023-01-04 10:38:42 -05:00
Ryan Cramer
68feea5d4e Fix issue processwire/processwire-issues#1661 2023-01-04 09:51:55 -05:00
Ryan Cramer
5ba7706747 Bump version to 3.0.209 2022-12-30 13:50:45 -05:00
Ryan Cramer
483b63d496 Various minor updates 2022-12-30 12:24:04 -05:00
Ryan Cramer
28bd6cdeec Minor code optimizations to InputfieldImage 2022-12-30 12:00:35 -05:00
Ryan Cramer
77c7e401da Attempt fix for processwire/processwire-issues#1600 2022-12-30 11:47:45 -05:00
Ryan Cramer
f57a07e15a Attempt fix for processwire/processwire-issues#1299 using @adrianbj suggestion 2022-12-29 12:20:20 -05:00
Ryan Cramer
1b76b92f8c Update CKEditor links per processwire/processwire-issues#797 @napengam 2022-12-29 10:13:09 -05:00
Ryan Cramer
90fc86f7cb Updates for processwire/processwire-issues#1588 2022-12-29 09:55:12 -05:00
Ryan Cramer
d6c54bda6a Attempt fix for processwire/processwire-issues#1467 maxFilesize PHP 8.2 notice per @matjazpotocnik 2022-12-29 09:31:59 -05:00
Ryan Cramer
b7e5d338f7 Fix issue processwire/processwire-issues#1651 2022-12-29 08:51:13 -05:00
Ryan Cramer
7e25c73728 Fix issue processwire/processwire-issues#1658 using solution suggested by @netcarver 2022-12-29 08:24:17 -05:00
Ryan Cramer
6018c1fbc4 Fix issue processwire/processwire-issues#1653 2022-12-09 13:35:50 -05:00
Ryan Cramer
adcee84904 Fix issue processwire/processwire-issues#1652 2022-12-09 13:16:04 -05:00
Ryan Cramer
a3cc73bd87 Bump version to 3.0.208, plus minor optimizations in FieldtypePage 2022-12-02 14:55:29 -05:00
Ryan Cramer
adf43d28df Several improvements to Notices: 1) now you can include separate label and notice text by calling message/warning/error like this: $this->message([ 'Notice label' => 'Notice text' ]); 2) new support for a Notice::allowDuplicate flag which allows duplicate notices to appear when rendered. To use, specify it in the flags argument: $this->message('notice text', 'duplicate'); or $this->message('notice text', Notice::duplicate); 3) improved debugging output when passed object or array values for message text. 4) notice icon can now be specified in the notice flags, if using a flags string, i.e. $this->message('notice text', 'icon-eye-slash'); These can be combined with other flags of course, i.e. $this->message('notice text', 'markdown duplicate icon-home'); 2022-12-02 14:07:41 -05:00
Ryan Cramer
a8b3c772d9 Add a Debug::toStr() method for dumping any variable to a string (for debugging purposes) 2022-12-02 14:04:53 -05:00
Ryan Cramer
b85c1c48b0 Fix issue processwire/processwire-issues#1649 2022-12-02 10:45:13 -05:00
Ryan Cramer
bceb0160b7 Attempt fix for processwire/processwire-issues#1645 2022-12-02 08:13:06 -05:00
Ryan Cramer
56e999dddb Updates for processwire/processwire-issues#1643 2022-12-02 08:02:49 -05:00
Ryan Cramer
b18ce642ef Add hookable getPage() method to PageFrontEdit module 2022-11-30 09:42:08 -05:00
Ryan Cramer
9c3d03c4a1 Various minor updates 2022-11-25 14:27:36 -05:00
Ryan Cramer
9bb9d71729 Update $sanitizer->email() method to add IDN, UTF-8, ASCII, and DNS support options. Also updates for processwire/processwire-issues#1647 2022-11-24 10:58:15 -05:00
Ryan Cramer
3e9d8e95da Fix issue processwire/processwire-issues#1648 2022-11-23 12:07:31 -05:00
Ryan Cramer
59cf5040f1 Update PageFrontEdit module to add support for InputfieldTinyMCE. Requires v6.0.7+ of InputfieldTinyMCE. cc @BernhardBaumrock 2022-11-22 12:15:40 -05:00
Ryan Cramer
c36adc9397 Minor updates to ProcessTemplate, with the most noticable addition being that when you add one (1) new template, it now goes straight to edit the template, rather than back to the template list. 2022-11-18 15:21:30 -05:00
Ryan Cramer
b0d5f14a27 Fixes for processwire/processwire-issues#1467 2022-11-18 12:42:02 -05:00
Ryan Cramer
2aedd2b6f5 Docs typo fixes per processwire/processwire-issues#797 2022-11-18 12:17:23 -05:00
Ryan Cramer
3d1c64f7e0 Update core dev JqueryCore version from 1.11.1 to 1.12.4 per processwire/processwire-issues#1643 2022-11-18 11:39:50 -05:00
Ryan Cramer
880ead3521 Bump version to 3.0.207 2022-11-11 14:06:16 -05:00
Ryan Cramer
ca344c838d Update $templates->delete($template) method to also delete fieldgroup (when not used elsewhere) 2022-11-11 14:04:56 -05:00
Ryan Cramer
eb94a38f75 Minor code updates in PagefilesManager 2022-11-11 13:59:27 -05:00
Ryan Cramer
aa842d8741 Fix issue processwire/processwire-issues#1638 2022-11-10 11:52:59 -05:00
Ryan Cramer
5aa0126b39 Fix issue processwire/processwire-issues#1637
Co-authored-by: BernhardBaumrock <office@baumrock.com>
2022-11-10 11:44:29 -05:00
Ryan Cramer
78c2ca736d Fix issue processwire/processwire-issues#1629 2022-11-10 11:05:05 -05:00
Ryan Cramer
9f39514376 Attempt fix for processwire/processwire-issues#1610 2022-11-10 10:40:53 -05:00
Ryan Cramer
c2eaa0a8da Fix issue processwire/processwire-issues#1632 with suggestion by @Notanotherdotcom 2022-11-04 15:42:27 -04:00
Ryan Cramer
d9bfba5c74 Fix issue processwire/processwire-issues#1630 2022-11-04 15:34:27 -04:00
Ryan Cramer
3d5561c7af Minor adjustment in LanguageTabs.js 2022-11-04 14:50:53 -04:00
Ryan Cramer
b4124435e4 Remove/comment-out some old code referring to TinyMCE 3.x in InputfieldRepeater.js 2022-11-04 14:34:01 -04:00
Ryan Cramer
715f029657 Minor code optimizations in Modules class plus fix processwire/processwire-issues#1634 as mentioned by @Toutouwai 2022-11-04 14:30:51 -04:00
BernhardBaumrock
701d24258c Updates to rock.less per PR #245 2022-10-13 10:29:41 -04:00
Ryan Cramer
050c9c19c3 Bump version to 3.0.206 2022-10-09 09:30:38 -04:00
Ryan Cramer
ec8555545e Update ProcessLogin to auto-refresh the login form every 5 minutes of non-activity. This helps to avoid cases where a login form sits for a long period and has its CSRF key expire. It can prevent this error at login: "This request was aborted because it appears to be forged." 2022-10-06 13:31:51 -04:00
Ryan Cramer
47f24e6ff0 Add allowProcessInput() hook to InputfieldWrapper class. This enables hooks to control whether a given Inputfield can be excluded from input processing at runtime 2022-10-06 13:21:09 -04:00
Ryan Cramer
d2bdf70ecd Minor JS adjustments 2022-10-06 13:20:17 -04:00
Ryan Cramer
96bb90bb2e Update inputfields.js for better error handling and improved reload support 2022-10-06 13:14:55 -04:00
Ryan Cramer
32d7a09cfd Add JS events to InputfieldRepeater: 'repeaterdelete' and 'repeaterundelete' 2022-09-30 12:04:44 -04:00
Ryan Cramer
c1e1cecf93 Minor code updates in FieldtypeOptions and supporting classes 2022-09-30 12:04:03 -04:00
Ryan Cramer
cce5073969 Fix example in InputfieldTextTags for cases where it is used without a Fieldtype 2022-09-30 12:02:53 -04:00
Ryan Cramer
5d86dc8dec Add support for a $page->getPageListLabel(); method which enables custom page classes to return custom markup for the page's label in the page-list. 2022-09-30 12:01:46 -04:00
Ryan Cramer
127ef175b7 Add support for $items[] = $item; append syntax to WireArray/PageArray 2022-09-30 12:00:02 -04:00
Ryan Cramer
a3e67b299b Add support for a data-field-name attribute on Inputfields that use names that differ from the relevant field name (likes inputfields in repeaters). This makes the source field discoverable from JS in cases where it previously wasn't. 2022-09-30 11:57:08 -04:00
Ryan Cramer
bd6b63c616 Correct phpdoc for $wire var in site-blank profile 2022-09-30 11:55:09 -04:00
Ryan Cramer
b87566d0b3 Minor code improvements and housekeeping for various core classes and modules 2022-09-23 11:19:51 -04:00
Ryan Cramer
d6d68d8cc0 Also add memory_limit check to SystemUpdaterChecks (system checks performed after superuser login) related to processwire/processwire-requests#449 2022-09-23 09:45:44 -04:00
Ryan Cramer
ab7c86da21 Add memory_limit check to PW installer per processwire/processwire-requests#449 2022-09-23 09:25:48 -04:00
Ryan Cramer
c3ea89cf03 Add feature request processwire/processwire-requests#457 - support for returning template property values (name template.name, etc) in $pages->findRaw() 2022-09-20 12:20:14 -04:00
matjazpotocnik
f510392e7e Fix issue #1623 2022-09-20 11:20:18 -04:00
Ryan Cramer
df3a603d66 Bump version to 3.0.205, plus additional minor code updates/improvements in multiple classes 2022-09-16 14:08:57 -04:00
Ryan Cramer
8748c72696 Minor code and phpdoc improvements to various core classes 2022-09-16 13:14:54 -04:00
FlipZoomMedia
5fecec74ec Add PR #244 - remember language tabs focus between requests after a double-click on a language tab 2022-09-16 11:46:32 -04:00
Ryan Cramer
96848654a4 Add PR #228 loop code optimization plus other minor code adjustments in WireFileTools
Co-authored-by: pine3ree <pine3ree@gmail.com>
2022-09-16 10:49:04 -04:00
pine3ree
330be8c05a Add PR #230 fix typo in comments of site-blank _init.php template file 2022-09-16 10:42:40 -04:00
pine3ree
a2614b1a46 Add PR #240 fix phpdoc return type in LanguagesPageFieldValue::getLanguageValue() method 2022-09-16 10:31:19 -04:00
dadish
d02efe55f2 Add PR #241 support pages.find() joinFields option for FieldtypeOptions fields 2022-09-16 09:24:34 -04:00
Ryan Cramer
71e9c50a57 Fix issue processwire/processwire-issues#1618 prevent exif_read_data call on non-jpg images in Pageimage.php 2022-09-16 09:08:43 -04:00
Ryan Cramer
2d29cf935e Fix issue processwire/processwire-issues#1619 by improving the error message when encountering an unsupported name for a module 2022-09-16 08:57:34 -04:00
Ryan Cramer
7711418a12 Update admin.php ajax response to populate new argument in ProcessController::jsonMessage() 2022-09-12 11:26:55 -04:00
Ryan Cramer
95bdbf76ba Add a csrf check to the Lister bookmarks form and make markup disallowed by default (with optional argument to enable it) in ProcessController ajax notification response generator (as used by some Lister errors).
Co-authored-by: filipaze <filipaze98@gmail.com>
Co-authored-by: rondons <guilhermetamagnini@gmail.com>
2022-09-12 11:24:05 -04:00
Ryan Cramer
f6558c25ac Minor code improvements in various classes, mostly phpdoc related 2022-09-12 11:15:53 -04:00
Ryan Cramer
6262fcdff8 Code improvements to ProcessPageEdit module and refactor of change-template form within it. 2022-09-12 09:36:01 -04:00
Ryan Cramer
4aa75a7d17 Update LanguageSupportPageNames module to support fallback to default pagination prefix 'page' when in the admin. This is to resolve an issue in Lister when the default language pagination prefix has been overridden. 2022-09-11 10:12:17 -04:00
Ryan Cramer
818d86feea Minor code updates in ProcessPageListerBookmarks 2022-09-09 14:58:50 -04:00
Ryan Cramer
a04548a814 Update InputfieldWrapper to have an error method that enables you get get Inputfields having errors rather than just getting error messages. 2022-09-09 13:51:36 -04:00
Ryan Cramer
cb026ccc4e Improvements to InputfieldForm module, including a new isSubmitted() method for a better way to check form submission, among other minor updates. 2022-09-09 13:49:25 -04:00
Ryan Cramer
94ca2b8e9a Fix issue where repeater family-friendly depth setting wasn't working when depth setting in field config was set to 1. Previously it only worked when depth setting was set to 2 or higher. 2022-09-05 09:24:07 -04:00
Ryan Cramer
6ca5eac61b Fix additional issues introduced on Friday in Page and PageValues classes 2022-09-04 10:05:23 -04:00
Ryan Cramer
68badf4658 Fix PagesLoaderCache error that appears when TravyDebugger installed 2022-09-04 09:08:40 -04:00
Ryan Cramer
ad4e359bca Major refactor of base Page class, moving a lot of code into a separate PageValues class, reducing the size of the base Page class. Also added new options for the get() method including full support of dot syntax (regardless of output formatting state) (i.e. "field_name.subfield_name.title") and support for brackets[] to force multi-value (i.e. "field_name[]") , indexes (i.e. field_name[0] to get first item), and bracketed selectors (i.e. "some_multi_value_field[title%=design]") and the ability to combine any of these. 2022-09-02 13:14:35 -04:00
Ryan Cramer
a2da7f1a22 Various minor updates 2022-09-02 13:13:08 -04:00
Ryan Cramer
96dae07160 Update to allow for Combo fields within Repeaters to support file/image fields 2022-08-28 10:43:44 -04:00
Ryan Cramer
cbb5133d46 Bump version to 3.0.204 2022-08-26 14:01:35 -04:00
Ryan Cramer
e8219ba71a Additional updates for PHP 8.1+ processwire/processwire-issues#1467 2022-08-26 12:19:06 -04:00
Ryan Cramer
866f91424f Fix issue processwire/processwire-issues#1611 where single-language site using multi-language fields in repeaters were not saving 2022-08-26 11:38:39 -04:00
Ryan Cramer
36702d7b57 Upgrades to Inputfield class: new renderFlags option letting you specify that a given Inputfield should render at the top of bottom of its siblings, regardless of its position (like a sticky attribute); new methods to support adding classes to different elements (wrap, header, content, input) in one call via a string (see addClass method phpdoc for details); new ability to configure classes for for those elements interactively in the Inputfield configuration, which can be useful in FormBuilder, LoginRegisterPro, and other instances; various minor code improvements as well. 2022-08-26 10:39:09 -04:00
Ryan Cramer
647c45d454 Adjustments in ProcessPageEditImageSelect 2022-08-19 15:00:58 -04:00
Ryan Cramer
79cfdd5f38 Minor adjustments in PagesEditor class 2022-08-19 15:00:07 -04:00
Ryan Cramer
38b2dd0732 Minor adjustments and phpdoc updates in Pagefile/Pageimage and related classes 2022-08-19 14:59:41 -04:00
Ryan Cramer
5f0da8067c Minor adjustments and phpdoc updates in Fieldtype and FieldtypeMulti base classes 2022-08-19 14:58:02 -04:00
Ryan Cramer
d0acf570a3 Add FieldtypeHasPagefiles and FieldtypeHasPageimages interfaces and update FieldtypeFile/Image to support them. These interfaces enable a Fieldtype communicate that it manages Pagefile/Pageimage objects and provides a method for retrieving them. 2022-08-19 14:56:36 -04:00
Ryan Cramer
8303efd303 Fix issue processwire/processwire-issues#1605 2022-08-14 10:09:10 -04:00
Ryan Cramer
9bb55785f8 Add owner selector support in ProcessPageSearch per request and code suggestion 2022-08-12 13:35:00 -04:00
Ryan Cramer
03de771b53 Various minor updates, mostly phpdoc related 2022-08-12 13:34:42 -04:00
Ryan Cramer
93b88c6ec3 Additional updates in InputfieldFile to support CKEditor custom fields in files/images. 2022-08-11 10:25:16 -04:00
Ryan Cramer
8daaedc809 Fix issue processwire/processwire-issues#1603 2022-08-05 12:21:26 -04:00
Ryan Cramer
79eebb5210 Additional minor updates to ProcessCommentsManager 2022-08-04 07:55:25 -04:00
Ryan Cramer
eec0975eda Add fix/update for processwire/processwire-issues#1597 using suggested fix provided by @Toutouwai 2022-07-29 12:53:48 -04:00
Ryan Cramer
7fd517dcb4 Add fix for processwire/processwire-issues#1602 using fix provided by @Toutouwai 2022-07-29 12:41:12 -04:00
Ryan Cramer
f27f11fbf6 New additions to ProcessCommentsManager including a new separate dedicated comment editor and the ability to add new comments or replies from within the module. There are still more additions to make to the dedicated comment editor including the ability to select page and parent (though these are already available in the comment list editing screen in this module). 2022-07-29 11:43:48 -04:00
Ryan Cramer
92fefa5476 Update comments.js to allow for de-selection of stars in cases where a user might accidentally click on a stars input and want to revert to no-selection. 2022-07-29 11:37:49 -04:00
Ryan Cramer
94babead9d Various minor updates 2022-07-29 11:37:32 -04:00
Ryan Cramer
67d937b081 Add $hookable option to wireMethodExists() function, enabling it to detect cases where the method may exist in a ___hookable() variation 2022-07-29 11:35:04 -04:00
Ryan Cramer
f71c417c15 Add new allowChildren() method to Comment class indicating whether or not replies are allowed to the comment. Update the editUrl() method to point to ProcessCommentsManager comment editor, when installed. 2022-07-29 11:33:52 -04:00
Ryan Cramer
fe9417dcfc Minor update to CommentArray 2022-07-16 15:03:42 -04:00
Ryan Cramer
25c110818e Bump version to 3.0.203 2022-07-15 16:08:42 -04:00
Ryan Cramer
69a63c1572 Various minor code and documentation updates 2022-07-15 15:30:08 -04:00
Ryan Cramer
c48ed61e06 Upgrade CKEditor version to 4.19.0 2022-07-15 15:27:55 -04:00
Ryan Cramer
2e6dc4426b Upgrade FieldtypeComments to support custom fields via new meta functions. Also upgrade ProcessCommentsManager and InputfieldCommentsAdmin for support. InputfieldCommentsAdmin was also upgraded with an option to link to the comments manager rather than making comments editable in the page editor (see "Input" tab for that config option). 2022-07-15 15:24:00 -04:00
Ryan Cramer
969dc8ebc4 Bump version to 3.0.202 2022-07-08 16:12:47 -04:00
Ryan Cramer
c1aa04b1fe Update for processwire/processwire-issues#1341 add inputmode=decimal attribute to FieldtypeFloat when in decimal input mode 2022-07-08 13:12:07 -04:00
Ryan Cramer
2da2c45382 Additional updates for processwire/processwire-issues#1467 2022-07-08 13:02:33 -04:00
Ryan Cramer
03acb77306 Fix issue processwire/processwire-issues#1595 2022-07-08 12:13:59 -04:00
Ryan Cramer
5d9ee59036 Attempt fix for processwire/processwire-issues#1594 2022-07-08 12:09:18 -04:00
Ryan Cramer
2e76df1a89 Fix issue processwire/processwire-issues#1593 2022-07-08 11:24:29 -04:00
Ryan Cramer
6a9dfe654d Update in WireHttp to correct issue where curl POST method sometimes did not work due to 'expect' header that CURL adds to some requests, plus an extra boundary CURL adds to content-type 2022-07-08 09:43:14 -04:00
Ryan Cramer
d4e2a39940 Update PagesPathFinder to provide 301 redirect for paths ending with "/index.php" rather than responding with a 404 code 2022-07-08 09:30:11 -04:00
Ryan Cramer
31ede89079 Minor updates in Debug class including documentation additions and removeSavedTimer methods 2022-07-08 09:28:26 -04:00
Ryan Cramer
e6a29cb051 Fix issue in WireInput where setUrlSegments() didn't reset all segments when given array of segments smaller than what was previously set. 2022-07-08 09:26:57 -04:00
Ryan Cramer
af950078ed Typo fix in module description for PagePathHistory 2022-06-24 13:32:16 -04:00
Ryan Cramer
f4b938146d Fix issue in the new ProcessPageList "hidden" pages option where it wasn't always working as intended. 2022-06-24 13:31:32 -04:00
Ryan Cramer
818b78a42c Fix issue in WireHttp where it didn't reset the HTTP code description text (like "OK" or "Page Not Found" or "Internal Server Error" between multiple requests. 2022-06-24 13:30:22 -04:00
Ryan Cramer
5d3367846e Fix issue processwire/processwire-issues#1588 2022-06-24 11:07:19 -04:00
Ryan Cramer
c697be7ddd Add partial fix for processwire/processwire-issues#1586
Co-authored-by: pine3ree <pine3ree@gmail.com>
2022-06-24 10:49:26 -04:00
Ryan Cramer
116219292c Fix issue processwire/processwire-issues#1584 2022-06-24 10:29:20 -04:00
Ryan Cramer
b46b29aa8e Fix issue processwire/processwire-issues#1582 2022-06-24 10:04:49 -04:00
Ryan Cramer
4f63edf25e Fix issue processwire/processwire-issues#1575 2022-06-24 09:31:44 -04:00
Ryan Cramer
356cde2d5c Add empty check to Page::get() method to avoid potential illegal offset-type error 2022-06-20 16:03:32 -04:00
Ryan Cramer
be134e1e65 Fix issue in InputfieldSelect.module where optionLabel() method didn't return labels for option values within optgroups 2022-06-20 15:57:40 -04:00
Ryan Cramer
773a344a2f Fix issue in ProcessPageLister where clicking column headings to sort results didn't work when in minimal (modal or frame) mode. 2022-06-03 15:00:33 -04:00
Ryan Cramer
431944c3b2 Add new configuration option to ProcessPageList where you can select pages that should not be shown in the page list (optionally conditional). This is useful for hiding pages like "404 Page Not Found", "Admin" and others that you may not need to appear in the page list. 2022-06-03 14:58:20 -04:00
Ryan Cramer
73a31ef2f9 Add processwire/processwire-requests#445 - Add usage fieldset/info on field edit page primary tab 2022-06-03 14:55:45 -04:00
Ryan Cramer
f0d06cbf8a Bump version to 3.0.201 2022-05-27 13:09:14 -04:00
Ryan Cramer
8d57d0c126 Fix typo in $page->getMultiple() method phpdoc 2022-05-27 11:20:04 -04:00
Ryan Cramer
f0bc888355 Fix issue processwire/processwire-issues#1576 2022-05-27 11:00:58 -04:00
Ryan Cramer
2cf811f76c Fix issue processwire/processwire-issues#1572 2022-05-27 10:47:26 -04:00
Ryan Cramer
6667caa1d0 Add new page editor Inputfield visibility mode 'Tab' which makes any Inputfield display as a page editor tab. Options included for 'Tab', 'Tab (AJAX)', and 'Tab (locked)'. 2022-05-27 10:40:58 -04:00
Ryan Cramer
23a4cb455d Add a new $page->getMultiple($keys); method where $keys can be array or CSV string of properties/fields to return in an array. Returns non-associative array by default, specify $page->getMultiple($keys, true); get to get can associative array indexed by given key names. 2022-05-27 10:35:12 -04:00
1524 changed files with 116848 additions and 47338 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 (2021), 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,20 +127,20 @@ 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.4.0 or newer required, 0.5.2 or newer recommended.
version 0.5.5 or newer recommended.
- [ListerPro](https://processwire.com/store/lister-pro/)
version 1.0.9 or newer required, 1.1.4 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/)
all versions supported but version 5 or newer recommended.
version 8 or newer recommended.
- [ProCache](https://processwire.com/store/pro-cache/)
version 3.1.4 or newer required, 4.0.0 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.
- For all other Pro modules not mentioned above (ProMailer, ProDrafts,
ProDevTools, Likes) there are no specific version requirements but we
recommend using the latest available versions when possible.
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.
- For all other Pro modules not mentioned above we recommend using the
latest available versions when possible.
## Debug Mode
@@ -156,12 +154,11 @@ we think you'll find it very handy during development or when resolving issues.
1. Edit this file: `/site/config.php`
2. Find this line: `$config->debug = false;`
3. Change the `false` to `true`, like below, and save.
3. Change the `false` to `true` like below, and save.
```
$config->debug = true;
```
This can be found near the bottom of the file, or you can add it if not
already there. It will make PHP and ProcessWire report all errors, warnings,
notices, etc. Of course, you'll want to set it back to false once you've
@@ -172,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 Twitter](http://twitter.com/processwire/)
* [Contact ProcessWire](https://processwire.com/contact/)
* [Contact ProcessWire developer](https://processwire.com/contact/)
* [Report issue](https://github.com/processwire/processwire-issues/issues)
------
Copyright 2022 by Ryan Cramer / Ryan Cramer Design, LLC
Copyright 2025 by Ryan Cramer / Ryan Cramer Design, LLC

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]
@@ -448,7 +448,7 @@ DirectoryIndex index.php index.html index.htm
RewriteCond %{REQUEST_FILENAME} !(favicon\.ico|robots\.txt)
# -----------------------------------------------------------------------------------------------
# 18. Optionally (O) prevent PW from attempting to serve images or anything in /site/assets/.
# 18. Optionally (O) prevent PW from serving some file types, or anything in /site/assets/.
# Both of these lines are optional, but can help to reduce server load. However, they
# are not compatible with the $config->pagefileSecure option (if enabled) and they
# may produce an Apache 404 rather than your regular 404. You may uncomment the two lines
@@ -458,7 +458,7 @@ DirectoryIndex index.php index.html index.htm
# section #2 above that makes ProcessWire the 404 handler.
# -----------------------------------------------------------------------------------------------
# RewriteCond %{REQUEST_URI} !\.(jpg|jpeg|gif|png|ico|webp|svg)$ [NC]
# RewriteCond %{REQUEST_URI} !\.(jpg|jpeg|gif|png|ico|webp|svg|js|css)$ [NC]
# RewriteCond %{REQUEST_FILENAME} !(^|/)site/assets/
# -----------------------------------------------------------------------------------------------
@@ -478,5 +478,4 @@ DirectoryIndex index.php index.html index.htm
#################################################################################################
# END PROCESSWIRE HTACCESS DIRECTIVES
#################################################################################################
#################################################################################################

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 2021 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
@@ -91,19 +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";
if($title && $formAction) {} // ignore
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 {
@@ -113,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);
@@ -188,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;
@@ -375,6 +375,16 @@ class Installer {
} else {
$this->warn("ZipArchive support was not found. This is recommended, but not required to complete installation.");
}
$memoryLimit = $this->getMemoryLimit('M');
$memoryLimitLabel = "PHP memory_limit is set to $memoryLimit MB";
if($memoryLimit < 64) {
$this->err("$memoryLimitLabel - At least 64 MB is strongly recommended but 128 MB or more is best");
} else if($memoryLimit < 128) {
$this->warn("$memoryLimitLabel - OK to continue, but at least 128 MB is recommended");
} else {
$this->ok("$memoryLimitLabel");
}
$dirs = array(
// directory => required?
@@ -390,14 +400,18 @@ class Installer {
} else if($required) {
$this->err("Directory $d must be writable. Please adjust the server permissions before continuing.");
} else {
$this->warn("We recommend that directory $d be made writable before continuing.");
$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")) {
@@ -465,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';
@@ -477,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')
);
@@ -657,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
@@ -791,7 +840,7 @@ class Installer {
$database->exec("CREATE SCHEMA IF NOT EXISTS `$dbName` DEFAULT CHARACTER SET `$dbCharset`");
// reconnect
$database = new \PDO($dsn, $values['dbUser'], $values['dbPass'], $driver_options);
if($database) $this->alertOk("Created database: $dbName");
$this->alertOk("Created database: $dbName");
} catch(\Exception $e) {
$this->alertErr("Failed to create database with name $dbName");
@@ -821,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)));
@@ -834,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 " .
@@ -855,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) " .
@@ -865,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" .
@@ -879,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 * " .
@@ -897,7 +965,7 @@ class Installer {
"\n\n";
if(!empty($values['httpHosts'])) {
$cfg .= "" .
$cfg .=
"\n/**" .
"\n * Installer: HTTP Hosts Whitelist" .
"\n * " .
@@ -918,7 +986,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");
@@ -1063,27 +1133,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();
@@ -1187,6 +1258,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/)",
@@ -1960,18 +2032,39 @@ class Installer {
'US Alaska|America/Anchorage',
'US Hawaii|America/Adak',
'US Hawaii (no DST)|Pacific/Honolulu',
);
);
foreach($extras as $t) $timezones[] = $t;
return $timezones;
}
/**
* Get memory limit
*
* @param string $getInUnit Get value in 'K' [kilobytes], 'M' [megabytes], 'G' [gigabytes] (default='M')
* @return int|float
* @since 3.0.206
*
*/
protected function getMemoryLimit($getInUnit = 'M') {
// $units = array('M' => 1048576, 'K' => 1024, 'G' => 1073741824);
$units = array('M' => 1000000, 'K' => 1000, 'G' => 1000000000);
$value = (string) ini_get('memory_limit');
$value = trim(strtoupper($value), ' B'); // KB=K, MB=>M, GB=G,
$unit = substr($value, -1); // K, M, G
$value = (int) rtrim($value, 'KMG');
if($unit === $getInUnit) return $value; // already in correct unit
if(isset($units[$unit])) $value = $value * $units[$unit]; // convert value to bytes
if(isset($units[$getInUnit])) $value = round($value / $units[$getInUnit]);
if(strpos("$value", '.') !== false) $value = round($value, 1);
return $value;
}
}
/****************************************************************************************************/
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 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
* https://processwire.com
*
*/
@@ -46,5 +46,4 @@ $config->appendTemplateFile = '_main.php';
// Allow template files to be compiled for backwards compatibility?
$config->templateCompile = false;
/*** INSTALLER CONFIG ********************************************************************/
/*** INSTALLER CONFIG ********************************************************************/

View File

@@ -2,7 +2,7 @@
if(!defined("PROCESSWIRE")) die();
/** @var Wire $wire */
/** @var ProcessWire $wire */
/**
* ProcessWire Request Finished

View File

@@ -2,7 +2,7 @@
if(!defined("PROCESSWIRE")) die();
/** @var Wire $wire */
/** @var ProcessWire $wire */
/**
* ProcessWire Bootstrap Initialization

View File

@@ -185,7 +185,6 @@ CREATE TABLE `modules` (
) ENGINE=MyISAM AUTO_INCREMENT=159 DEFAULT CHARSET=utf8;
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('1', 'FieldtypeTextarea', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('2', 'FieldtypeNumber', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('3', 'FieldtypeText', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('4', 'FieldtypePage', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('30', 'InputfieldForm', '0', '', NOW());
@@ -207,7 +206,6 @@ INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('28', '
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('29', 'FieldtypeEmail', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('108', 'InputfieldURL', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('32', 'InputfieldSubmit', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('33', 'InputfieldWrapper', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('34', 'InputfieldText', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('35', 'InputfieldTextarea', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('36', 'InputfieldSelect', '0', '', NOW());
@@ -218,7 +216,6 @@ INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('40', '
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('41', 'InputfieldName', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('43', 'InputfieldSelectMultiple', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('45', 'JqueryWireTabs', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('46', 'ProcessPage', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('47', 'ProcessTemplate', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('48', 'ProcessField', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('50', 'ProcessModule', '0', '', NOW());
@@ -267,7 +264,7 @@ INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('148',
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('149', 'InputfieldSelector', '2', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('150', 'ProcessPageLister', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('151', 'JqueryMagnific', '1', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('155', 'InputfieldCKEditor', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('155', 'InputfieldTinyMCE', '0', '', NOW());
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('156', 'MarkupHTMLPurifier', '0', '', NOW());
DROP TABLE IF EXISTS `pages`;

View File

@@ -2,7 +2,7 @@
if(!defined("PROCESSWIRE")) die();
/** @var Wire $wire */
/** @var ProcessWire $wire */
/**
* ProcessWire Bootstrap API Ready

View File

@@ -1,6 +1,6 @@
<?php namespace ProcessWire;
// Optional initialization file, called before rendering any template file.
// This is defined by $config->appendTemplateFile in /site/config.php.
// This is defined by $config->prependTemplateFile in /site/config.php.
// Use this to define shared variables, functions, classes, includes, etc.

View File

@@ -11,8 +11,8 @@
/** @var Page $page */
/** @var Pages $pages */
/** @var Config $config */
$home = $pages->get('/'); // homepage
$home = $pages->get('/'); /** @var HomePage $home */
?><!DOCTYPE html>
<html lang="en">
@@ -52,4 +52,4 @@ $home = $pages->get('/'); // homepage
<?php endif; ?>
</body>
</html>
</html>

View File

@@ -1,11 +1,15 @@
<?php namespace ProcessWire;
// Template file for pages using the “basic-page” template
// -------------------------------------------------------
// The #content div in this file will replace the #content div in _main.php
// when the Markup Regions feature is enabled, as it is by default.
// You can also append to (or prepend to) the #content div, and much more.
// See the Markup Regions documentation:
// https://processwire.com/docs/front-end/output/markup-regions/
?>
<div id="content">
Basic page content
</div>
</div>

View File

@@ -1,10 +1,15 @@
<?php namespace ProcessWire;
// Template file for “home” template used by the homepage
// ------------------------------------------------------
// The #content div in this file will replace the #content div in _main.php
// when the Markup Regions feature is enabled, as it is by default.
// You can also append to (or prepend to) the #content div, and much more.
// See the Markup Regions documentation:
// https://processwire.com/docs/front-end/output/markup-regions/
?>
<div id="content">
Homepage content
</div>
</div>

View File

@@ -18,11 +18,11 @@ insert_final_newline = true
[*.js]
indent_style = tab
trim_trailing_whitespace = true
trim_trailing_whitespace = false
insert_final_newline = true
[*.{css,less,scss}]
indent_style = tab
trim_trailing_whitespace = true
trim_trailing_whitespace = false
insert_final_newline = true

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
*
*
@@ -46,28 +46,40 @@ if(!defined("PROCESSWIRE")) die();
* always have this disabled for live/production sites since it reveals more information
* than is advisible for security.
*
* You may also set this to the constant `Config::debugVerbose` to enable verbose debug mode,
* which uses more memory and time.
* You may also set this to one of the constants:
* - `Config::debugVerbose` (or int `2`) for verbose debug mode, which uses more memory/time.
* - `Config::debugDev` (or string `dev`) for core development debug mode, which makes it use
* newer JS libraries in some cases when we are testing them.
*
* #notes This enables debug mode for ALL requests. See the debugIf option for an alternative.
*
* @var bool
* @var bool|string|int
*
*/
$config->debug = false;
/**
* Enable debug mode if condition is met
*
* ~~~~~
* $config->debug = false; // setting this to false required when using debugIf
* $config->debugIf = '123.123.123.123'; // true if user matches this IP address
* $config->debugIf = [ '123.123.123.123', '456.456.456.456' ]; // array of IPs (3.0.212)+
* $config->debugIf = 'function_name_to_call'; // callable function name
* $config->debugIf = function() { // callable function (3.0.212+)
* return $_SERVER['SERVER_PORT'] === '8888';
* };
* ~~~~~
*
* Set debug mode to be false above, and then specify any one of the following here:
* 1) IP address of user required to enable debug mode;
* 2) Your own callable function name (i.e. "debug_mode") in /site/config.php that returns
* true or false for debug mode;
* 3) PCRE regular expression to match IP address of user (must start and end with a "/"
* slash). If IP address matches, then debug mode is enabled. Regular expression
* example: /^123\.456\.789\./ would match all IP addresses that started with 123.456.789.
* - IP address of user required to enable debug mode;
* - Array of IP addresses where that debug mode should be enabled for (3.0.212+).
* - Your own callable function in /site/config.php that returns true or false for debug mode.
* - PCRE regular expression to match IP address of user (must start and end with a "/"
* slash). If IP address matches, then debug mode is enabled. Regular expression
* example: `/^123\.456\.789\./` would match all IP addresses that started with 123.456.789.
*
* #notes When used, this overrides $config->debug, changing it at runtime automatically.
* #notes When used, this will override a false $config->debug, changing it at runtime automatically.
* @var string
*
*/
@@ -214,6 +226,36 @@ $config->usePageClasses = false;
*/
$config->useLazyLoading = true;
/**
* Default value for $useVersion argument of $config->versionUrls() method
*
* Controls the cache busting behavior of the `$config->versionUrls()` method as used by
* ProcessWires admin themes (but may be used independently as well). When no
* `$useVersion` argument is specified to the versionUrls() method, it will use the
* default value specified here. If not specified, null is the default.
*
* - `true` (bool): Get version from filemtime.
* - `false` (bool): Never get file version, just use `$config->version`.
* - `foobar` (string): Specify any string to be the version to use on all URLs needing it.
* - `?foo=bar` (string): Optionally specify your own query string variable=value.
* - `null` (null): Auto-detect: use file version in debug mode or dev branch only,
* otherwise use `$config->version`.
*
* ~~~~~
* // choose one to start with, copy and place in /site/config.php to enable
* $config->useVersionUrls = null; // default setting
* $config->useVersionUrls = true; // always use filemtime based version URLs
* $config->useVersionUrls = false; // only use core version in URLs
* $config->versionUrls = 'hello-world'; // always use this string as the version
* $config->versionUrls = '?version=123'; // optionally specify query string var and value
* ~~~~~
*
* @var null|bool|string
* @since 3.0.227
*
* $config->useVersionUrls = null;
*/
/**
* Disable all HTTPS requirements?
*
@@ -304,8 +346,8 @@ $config->sessionExpireSeconds = 86400;
* Use this to determine at runtime whether or not a session is allowed for the current request.
* Otherwise, this should always be boolean TRUE. When using this option, we recommend
* providing a callable function like below. Make sure that you put in some logic to enable
* sessions on admin pages at minimum. The callable function receives a single $wire argument
* which is the ProcessWire instance.
* sessions on admin pages at minimum. The callable function receives a single $session argument
* which is the ProcessWire Session instance.
*
* Note that the API is not fully ready when this function is called, so the current $page and
* the current $user are not yet known, nor is the $input API variable available.
@@ -481,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
*
@@ -506,7 +560,7 @@ $config->loginDisabledRoles = array(
/**
* Allow template files to be compiled?
*
* Set to false do disable the option for compiled template files.
* Set to false to disable the option for compiled template files.
* When set to true, it will be used unless a given template's 'compile' option is set to 0.
* This setting also covers system status files like /site/ready.php, /site/init.php, etc. (3.0.142+)
*
@@ -688,7 +742,7 @@ $config->contentTypes = array(
'txt' => 'text/plain',
'json' => 'application/json',
'xml' => 'application/xml',
);
);
/**
* File content types
@@ -721,7 +775,7 @@ $config->fileContentTypes = array(
'webp' => 'image/webp',
'zip' => '+application/zip',
'mp3' => 'audio/mpeg',
);
);
/**
* Named predefined image sizes and options
@@ -785,7 +839,7 @@ $config->imageSizerOptions = array(
'hidpiQuality' => 60, // Same as above quality setting, but specific to hidpi images
'defaultGamma' => 2.0, // defaultGamma: 0.5 to 4.0 or -1 to disable gamma correction (default=2.0)
'webpAdd' => false, // set this to true, if the imagesizer engines should create a Webp copy with every (new) image variation
);
);
/**
* Options for webp images
@@ -803,7 +857,7 @@ $config->webpOptions = array(
'useSrcExt' => false, // Use source file extension in webp filename? (file.jpg.webp rather than file.webp)
'useSrcUrlOnSize' => true, // Fallback to source file URL when webp file is larger than source?
'useSrcUrlOnFail' => true, // Fallback to source file URL when webp file fails for some reason?
);
);
/**
* Admin thumbnail image options
@@ -834,7 +888,7 @@ $config->adminThumbOptions = array(
'sharpening' => 'soft', // sharpening: none | soft | medium | strong
'quality' => 90,
'suffix' => '',
);
);
/**
* File compiler options (as used by FileCompiler class)
@@ -863,7 +917,7 @@ $config->fileCompilerOptions = array(
'exclusions' => array(), // exclude filenames or paths that start with any of these
'extensions' => array('php', 'module', 'inc'), // file extensions we compile
'cachePath' => '', // path where compiled files are stored, or blank for $config->paths->cache . 'FileCompiler/'
);
);
/**
* Temporary directory for uploads
@@ -931,7 +985,7 @@ $config->protectCSRF = true;
* @var int
*
*/
$config->maxUrlSegments = 4;
$config->maxUrlSegments = 20;
/**
* Maximum length for any individual URL segment (default=128)
@@ -950,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
@@ -962,7 +1032,7 @@ $config->maxUrlDepth = 30;
* @var string
*
*/
$config->pageNumUrlPrefix = 'page';
$config->pageNumUrlPrefix = 'page'; // note that "-" is not a supported character in the prefix
/**
* Multiple prefixes that may be used for detecting pagination
@@ -995,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
*
*/
@@ -1357,16 +1435,14 @@ $config->moduleServiceKey = 'pw301';
* - `upload`: Allow installation by file upload?
* - `download`: Allow installation by file download from URL?
*
* @todo consider whether the 'directory' option should also be limited to 'debug' only.
*
* @var array
* @since 3.0.163
*
*/
$config->moduleInstall = array(
'directory' => true, // allow install from ProcessWire modules directory?
'upload' => 'debug', // allow install by module file upload?
'download' => 'debug', // allow install by download from URL?
'directory' => 'debug', // allow install from ProcessWire modules directory?
'upload' => false, // allow install by module file upload?
'download' => false, // allow install by download from URL?
);
/**
@@ -1652,14 +1728,15 @@ $config->modals = array(
* This is an optimization that can reduce some database overhead.
*
* @var array
* @deprecated No longer in use as of 3.0.218
*
*/
$config->preloadCacheNames = array(
'Modules.info',
//'Modules.info',
//'ModulesVerbose.info',
'ModulesVersions.info',
'Modules.wire/modules/',
'Modules.site/modules/',
//'ModulesVersions.info',
//'Modules.wire/modules/',
//'Modules.site/modules/',
);
/**

View File

@@ -10,7 +10,7 @@
* This file is licensed under the MIT license.
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
* @property int|string $version Current admin theme version
@@ -102,6 +102,7 @@ abstract class AdminTheme extends WireData implements Module {
*/
public function __construct() {
// placeholder
parent::__construct();
}
/**
@@ -292,7 +293,7 @@ abstract class AdminTheme extends WireData implements Module {
*
*/
protected function setCurrent() {
$config = $this->wire('config');
$config = $this->wire()->config;
$name = $this->className();
$config->paths->set('adminTemplates', $config->paths->get($name));
$config->urls->set('adminTemplates', $config->urls->get($name));
@@ -315,13 +316,14 @@ abstract class AdminTheme extends WireData implements Module {
*/
public function ___getExtraMarkup() {
$parts = $this->extraMarkup;
$isLoggedin = $this->wire('user')->isLoggedin();
if($isLoggedin && $this->wire('modules')->isInstalled('InputfieldCKEditor')
&& $this->wire('process') instanceof WirePageEditor) {
$isLoggedin = $this->wire()->user->isLoggedin();
if($isLoggedin && $this->wire()->modules->isInstalled('InputfieldCKEditor')
&& $this->wire()->process instanceof WirePageEditor) {
// necessary for when CKEditor is loaded via ajax
$parts['head'] .= "<script>" .
"window.CKEDITOR_BASEPATH='" . $this->wire('config')->urls->InputfieldCKEditor .
'ckeditor-' . InputfieldCKEditor::CKEDITOR_VERSION . "/';</script>";
$script = 'script';
$parts['head'] .= "<$script>" .
"window.CKEDITOR_BASEPATH='" . $this->wire()->config->urls('InputfieldCKEditor') .
'ckeditor-' . InputfieldCKEditor::CKEDITOR_VERSION . "/';</$script>";
}
/*
if($isLoggedin && $this->wire('config')->advanced) {
@@ -447,9 +449,12 @@ abstract class AdminTheme extends WireData implements Module {
// if we are the only admin theme installed, no need to add an admin_theme field
if(self::$numAdminThemes == 0) return;
$modules = $this->wire()->modules;
// install a field for selecting the admin theme from the user's profile
$field = $this->wire('fields')->get('admin_theme');
/** @var Field $field */
$field = $this->wire()->fields->get('admin_theme');
$toUseNote = $this->_('To use this theme, select it from your user profile.');
@@ -458,9 +463,10 @@ abstract class AdminTheme extends WireData implements Module {
$this->message($toUseNote);
} else {
// this will be the 2nd admin theme installed, so add a field that lets them select admin theme
/** @var Field $field */
$field = $this->wire(new Field());
$field->name = 'admin_theme';
$field->type = $this->wire('modules')->get('FieldtypeModule');
$field->type = $modules->get('FieldtypeModule');
$field->set('moduleTypes', array('AdminTheme'));
$field->set('labelField', 'title');
$field->set('inputfieldClass', 'InputfieldRadios');
@@ -475,7 +481,7 @@ abstract class AdminTheme extends WireData implements Module {
if($field && $field->id) {
/** @var Fieldgroup $fieldgroup */
$fieldgroup = $this->wire('fieldgroups')->get('user');
$fieldgroup = $this->wire()->fieldgroups->get('user');
if(!$fieldgroup->hasField($field)) {
$fieldgroup->add($field);
$fieldgroup->save();
@@ -483,9 +489,9 @@ abstract class AdminTheme extends WireData implements Module {
$this->message($toUseNote);
}
// make this field one that the user is allowed to configure in their profile
$data = $this->wire('modules')->getModuleConfigData('ProcessProfile');
$data = $modules->getModuleConfigData('ProcessProfile');
$data['profileFields'][] = 'admin_theme';
$this->wire('modules')->saveModuleConfigData('ProcessProfile', $data);
$modules->saveModuleConfigData('ProcessProfile', $data);
}
}
@@ -505,7 +511,7 @@ abstract class AdminTheme extends WireData implements Module {
public function ___uninstall() {
$defaultAdminTheme = $this->wire('config')->defaultAdminTheme;
$defaultAdminTheme = $this->wire()->config->defaultAdminTheme;
if($defaultAdminTheme == $this->className()) {
throw new WireException(
"Cannot uninstall this admin theme because \$config->defaultAdminTheme = '$defaultAdminTheme'; " .

View File

@@ -17,6 +17,7 @@
* @property bool $isLoggedIn
* @property bool|string $isModal
* @property bool|int $useAsLogin
* @property string $browserTitle Optional custom browser title for this request (3.0.217+)
* @method array getUserNavArray()
* @method array getPrimaryNavArray()
* @method string renderFile($basename, array $vars = [])
@@ -93,10 +94,11 @@ abstract class AdminThemeFramework extends AdminTheme {
public function __construct() {
parent::__construct();
$this->set('useAsLogin', false);
$this->set('browserTitle', '');
}
public function wired() {
$this->sanitizer = $this->wire('sanitizer');
$this->sanitizer = $this->wire()->sanitizer;
$user = $this->wire()->user;
$this->isLoggedIn = $user && $user->isLoggedin();
parent::wired();
@@ -128,7 +130,9 @@ abstract class AdminThemeFramework extends AdminTheme {
*/
public function init() {
$user = $this->wire('user');
$user = $this->wire()->user;
$input = $this->wire()->input;
if(!$this->isLoggedIn && $this->useAsLogin) $this->setCurrent();
parent::init();
@@ -139,11 +143,11 @@ abstract class AdminThemeFramework extends AdminTheme {
$this->isEditor = $this->isLoggedIn && ($this->isSuperuser || $user->hasPermission('page-edit'));
$this->includeInitFile();
$modal = $this->wire('input')->get('modal');
$modal = $input->get('modal');
if($modal) $this->isModal = $modal == 'inline' ? 'inline' : true;
// test notices when requested
if($this->wire('input')->get('test_notices') && $this->isLoggedIn) $this->testNotices();
if($input->get('test_notices') && $this->isLoggedIn) $this->testNotices();
}
/**
@@ -151,12 +155,12 @@ abstract class AdminThemeFramework extends AdminTheme {
*
*/
public function includeInitFile() {
$config = $this->wire('config');
$config = $this->wire()->config;
$initFile = $this->path() . 'init.php';
if(file_exists($initFile)) {
if(strpos($initFile, $config->paths->site) === 0) {
// admin themes in /site/modules/ may be compiled
$initFile = $this->wire('files')->compile($initFile);
$initFile = $this->wire()->files->compile($initFile);
}
/** @noinspection PhpIncludeInspection */
include_once($initFile);
@@ -173,9 +177,11 @@ abstract class AdminThemeFramework extends AdminTheme {
*/
public function _($text) {
static $translate = null;
if(is_null($translate)) $translate = $this->wire('languages') !== null;
static $context = null;
if($translate === null) $translate = $this->wire()->languages !== null;
if($translate === false) return $text;
$value = __($text, $this->wire('config')->paths->root . 'wire/templates-admin/default.php');
if($context === null) $context = $this->wire()->config->paths->root . 'wire/templates-admin/default.php';
$value = __($text, $context);
if($value === $text) $value = parent::_($text);
return $value;
}
@@ -187,9 +193,9 @@ abstract class AdminThemeFramework extends AdminTheme {
*
*/
public function getHeadline() {
$headline = $this->wire('processHeadline');
if(!$headline) $headline = $this->wire('page')->get('title|name');
if($headline !== 'en' && $this->wire('languages')) $headline = $this->_($headline);
$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);
}
@@ -225,7 +231,7 @@ abstract class AdminThemeFramework extends AdminTheme {
public function getPageIcon(Page $p) {
$icon = '';
if($p->template == 'admin') {
$info = $this->wire('modules')->getModuleInfo($p->process);
$info = $this->wire()->modules->getModuleInfo($p->process);
if(!empty($info['icon'])) $icon = $info['icon'];
}
// allow for option of an admin field overriding the module icon
@@ -236,7 +242,7 @@ abstract class AdminThemeFramework extends AdminTheme {
case 21: $icon = 'plug'; break; // Modules
case 28: $icon = 'key'; break; // Access
}
if(!$icon && $p->parent->id != $this->wire('config')->adminRootPageID) {
if(!$icon && $p->parent->id != $this->wire()->config->adminRootPageID) {
$icon = 'file-o ui-priority-secondary';
}
return $icon;
@@ -253,17 +259,17 @@ abstract class AdminThemeFramework extends AdminTheme {
*/
public function getAddNewActions() {
$page = $this->wire('page');
$process = $this->wire('process');
$input = $this->wire('input');
$page = $this->wire()->page;
$process = $this->wire()->process;
$input = $this->wire()->input;
if(!$this->isEditor) return array();
if($page->name != 'page' || $this->wire('input')->urlSegment1) return array();
if($page->name != 'page' && $page->name != 'list') return array();
if($input->urlSegment1 || $input->get('modal')) return array();
if(strpos($process, 'ProcessPageList') !== 0) return array();
/** @var ProcessPageAdd $module */
$module = $this->wire('modules')->getModule('ProcessPageAdd', array('noInit' => true));
$module = $this->wire()->modules->getModule('ProcessPageAdd', array('noInit' => true));
$data = $module->executeNavJSON(array('getArray' => true));
$actions = array();
@@ -293,8 +299,8 @@ abstract class AdminThemeFramework extends AdminTheme {
*/
public function getBodyClass() {
$page = $this->wire('page');
$process = $this->wire('process');
$page = $this->wire()->page;
$process = $this->wire()->process;
$classes = array(
"id-{$page->id}",
@@ -305,7 +311,7 @@ abstract class AdminThemeFramework extends AdminTheme {
if($this->isModal) $classes[] = 'modal';
if($this->isModal === 'inline') $classes[] = 'modal-inline';
if($this->wire('input')->urlSegment1) $classes[] = 'hasUrlSegments';
if($this->wire()->input->urlSegment1) $classes[] = 'hasUrlSegments';
if($process) $classes[] = $process->className();
if(!$this->isLoggedIn) $classes[] = 'pw-guest';
@@ -347,14 +353,15 @@ abstract class AdminThemeFramework extends AdminTheme {
if($p->process == 'ProcessPageAdd') {
// ProcessPageAdd: avoid showing this menu item if there are no predefined family settings to use
$numAddable = $this->wire('session')->getFor('ProcessPageAdd', 'numAddable');
$session = $this->wire()->session;
$numAddable = $session->getFor('ProcessPageAdd', 'numAddable');
if($numAddable === null) {
/** @var ProcessPageAdd $processPageAdd */
$processPageAdd = $this->wire('modules')->getModule('ProcessPageAdd', array('noInit' => true));
$processPageAdd = $this->wire()->modules->getModule('ProcessPageAdd', array('noInit' => true));
if($processPageAdd) {
$addData = $processPageAdd->executeNavJSON(array('getArray' => true));
$numAddable = $addData['list'];
$this->wire('session')->setFor('ProcessPageAdd', 'numAddable', $numAddable);
$session->setFor('ProcessPageAdd', 'numAddable', $numAddable);
}
}
// no addable options, so do not show the "Add New" item
@@ -365,23 +372,23 @@ abstract class AdminThemeFramework extends AdminTheme {
if(!$p->process) {
// no process module present, so we delegate to just the page viewable state if no children to check
if($pageViewable && !$numChildren) return true;
if(!$numChildren) return true;
} else if($p->process == 'ProcessList') {
// page just serves as a list for children
} else {
// determine permission from Process module, if present
$moduleInfo = $this->wire('modules')->getModuleInfo($p->process);
$moduleInfo = $this->wire()->modules->getModuleInfo($p->process);
if(!empty($moduleInfo['permission'])) $permission = $moduleInfo['permission'];
}
}
if($permission) {
// specific permission required to determine view access
$allow = $this->wire('user')->hasPermission($permission);
$allow = $this->wire()->user->hasPermission($permission);
} else if($pageViewable && $p->parent_id == $this->wire('config')->adminRootPageID) {
} else if($p->parent_id == $this->wire()->config->adminRootPageID) {
// primary nav page requires that at least one child is viewable
foreach($children as $child) {
if($this->allowPageInNav($child)) {
@@ -403,8 +410,8 @@ abstract class AdminThemeFramework extends AdminTheme {
public function ___getPrimaryNavArray() {
$items = array();
$config = $this->wire('config');
$admin = $this->wire('pages')->get($config->adminRootPageID);
$config = $this->wire()->config;
$admin = $this->wire()->pages->get($config->adminRootPageID);
foreach($admin->children("check_access=0") as $p) {
$item = $this->pageToNavArray($p);
@@ -424,10 +431,11 @@ abstract class AdminThemeFramework extends AdminTheme {
*/
public function moduleToNavArray($module, Page $p) {
$config = $this->wire('config');
$modules = $this->wire('modules');
$config = $this->wire()->config;
$modules = $this->wire()->modules;
$user = $this->wire()->user;
$textdomain = str_replace($config->paths->root, '/', $modules->getModuleFile($p->process));
$user = $this->wire('user');
$navArray = array();
if(is_array($module)) {
@@ -484,7 +492,7 @@ abstract class AdminThemeFramework extends AdminTheme {
// no children available
if($p->template == 'admin' && $p->process) {
// see if process module defines its own navigation
$moduleInfo = $this->wire('modules')->getModuleInfo($p->process);
$moduleInfo = $this->wire()->modules->getModuleInfo($p->process);
if(!empty($moduleInfo['nav'])) {
$navArray['children'] = $this->moduleToNavArray($moduleInfo, $p);
}
@@ -499,7 +507,7 @@ abstract class AdminThemeFramework extends AdminTheme {
// if we reach this point, then we have a PageArray of children
$modules = $this->wire('modules');
$modules = $this->wire()->modules;
foreach($children as $c) {
@@ -540,7 +548,7 @@ abstract class AdminThemeFramework extends AdminTheme {
*
*/
public function ___getUserNavArray() {
$urls = $this->wire('urls');
$urls = $this->wire()->urls;
$navArray = array();
$navArray[] = array(
@@ -550,7 +558,7 @@ abstract class AdminThemeFramework extends AdminTheme {
'icon' => 'eye',
);
if($this->wire('user')->hasPermission('profile-edit')) $navArray[] = array(
if($this->wire()->user->hasPermission('profile-edit')) $navArray[] = array(
'url' => $urls->admin . 'profile/',
'title' => $this->_('Profile'),
'icon' => 'user',
@@ -574,17 +582,20 @@ abstract class AdminThemeFramework extends AdminTheme {
*
*/
public function getBrowserTitle() {
$browserTitle = $this->browserTitle; // custom defined browser title
if(strlen($browserTitle)) return $this->sanitizer->entities($browserTitle);
$browserTitle = $this->wire('processBrowserTitle');
$modal = $this->wire('input')->get('modal');
$modal = $this->wire()->input->get('modal');
if(!$browserTitle) {
if($modal) return $this->wire('processHeadline');
$browserTitle = $this->_(strip_tags($this->wire('page')->get('title|name'))) . ' • ProcessWire';
$browserTitle = $this->_(strip_tags($this->wire()->page->get('title|name'))) . ' • ProcessWire';
}
if(!$modal) {
$httpHost = $this->wire('config')->httpHost;
$httpHost = $this->wire()->config->httpHost;
if(strpos($httpHost, 'www.') === 0) $httpHost = substr($httpHost, 4); // remove www
if(strpos($httpHost, ':')) $httpHost = preg_replace('/:\d+/', '', $httpHost); // remove port
$browserTitle .= "$httpHost";
@@ -600,7 +611,7 @@ abstract class AdminThemeFramework extends AdminTheme {
*
*/
public function testNotices() {
if(!$this->wire('user')->isLoggedin()) return false;
if(!$this->wire()->user->isLoggedin()) return false;
$this->message('Message test');
$this->message('Message test debug', Notice::debug);
$this->message('Message test markup <a href="#">example</a>', Notice::allowMarkup);
@@ -648,7 +659,7 @@ abstract class AdminThemeFramework extends AdminTheme {
$options = array_merge($defaults, $options);
if($notices === true) return $options;
$config = $this->wire('config');
$config = $this->wire()->config;
$noticesArray = array();
$out = '';
@@ -656,15 +667,16 @@ abstract class AdminThemeFramework extends AdminTheme {
$removeLabel = $this->_('Close all');
$removeLink = "<a class='$options[closeClass]' href='#' title='$removeLabel'>$removeIcon</a>";
if($this->isLoggedIn && $this->wire('modules')->isInstalled('SystemNotifications')) {
if($this->isLoggedIn && $this->wire()->modules->isInstalled('SystemNotifications')) {
$defaults['groupByType'] = false;
//$systemNotifications = $this->wire('modules')->get('SystemNotifications');
//if(!$systemNotifications->placement) return '';
}
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);
@@ -930,4 +942,3 @@ abstract class AdminThemeFramework extends AdminTheme {
}
}

View File

@@ -11,5 +11,6 @@ class Breadcrumb extends WireData {
$this->set('url', $url);
$this->set('title', $title);
$this->set('titleMarkup', '');
parent::__construct();
}
}
}

View File

@@ -12,7 +12,7 @@
* This file is licensed under the MIT license
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
*/
@@ -48,6 +48,8 @@ class CacheFile extends Wire {
*
*/
public function __construct($path, $id, $cacheTimeSeconds) {
parent::__construct();
$this->useFuel(false);
$path = rtrim($path, '/') . '/';
@@ -55,11 +57,11 @@ class CacheFile extends Wire {
$this->path = $id ? $path . $id . '/' : $path;
if(!is_dir($path)) {
if(!$this->wire('files')->mkdir($path, true)) throw new WireException("Unable to create path: $path");
if(!wire()->files->mkdir($path, true)) throw new WireException("Unable to create path: $path");
}
if(!is_dir($this->path)) {
if(!$this->wire('files')->mkdir($this->path)) throw new WireException("Unable to create path: {$this->path}");
if(!wire()->files->mkdir($this->path)) throw new WireException("Unable to create path: $this->path");
}
if(is_file($this->globalExpireFile)) {
@@ -97,8 +99,11 @@ class CacheFile extends Wire {
*/
protected function buildFilename() {
$filename = $this->path;
if($this->secondaryID) $filename .= $this->secondaryID;
else $filename .= $this->primaryID;
if($this->secondaryID) {
$filename .= $this->secondaryID;
} else {
$filename .= $this->primaryID;
}
$filename .= self::cacheFileExtension;
return $filename;
}
@@ -189,12 +194,12 @@ class CacheFile extends Wire {
return false;
}
} else {
$this->wire('files')->mkdir("$dirname/", true);
$this->wire()->files->mkdir("$dirname/", true);
}
}
$result = file_put_contents($filename, $data);
$this->wire('files')->chmod($filename);
$this->wire()->files->chmod($filename);
return $result;
}
@@ -210,7 +215,7 @@ class CacheFile extends Wire {
foreach($dir as $file) {
if($file->isDir() || $file->isDot()) continue;
//if(strpos($file->getFilename(), self::cacheFileExtension)) @unlink($file->getPathname());
if(self::isCacheFile($file->getPathname())) $this->wire('files')->unlink($file->getPathname());
if(self::isCacheFile($file->getPathname())) $this->wire()->files->unlink($file->getPathname());
}
return @rmdir($this->path);
@@ -223,7 +228,7 @@ class CacheFile extends Wire {
*
*/
protected function removeFilename($filename) {
$this->wire('files')->unlink($filename);
$this->wire()->files->unlink($filename);
}
@@ -239,6 +244,7 @@ class CacheFile extends Wire {
$dir = new \DirectoryIterator($path);
$numRemoved = 0;
$files = wire()->files;
foreach($dir as $file) {
@@ -250,7 +256,7 @@ class CacheFile extends Wire {
$numRemoved += self::removeAll($pathname, true);
} else if($file->isFile() && (self::isCacheFile($pathname) || ($file->getFilename() == self::globalExpireFilename))) {
if(wire('files')->unlink($pathname)) $numRemoved++;
if($files->unlink($pathname)) $numRemoved++;
}
}
@@ -266,7 +272,8 @@ class CacheFile extends Wire {
*
*/
public function expireAll() {
$note = "The modification time of this file represents the time of the last usable cache file. " .
$note =
"The modification time of this file represents the time of the last usable cache file. " .
"Cache files older than this file are considered expired. " . date('m/d/y H:i:s');
@file_put_contents($this->globalExpireFile, $note, LOCK_EX);
}

View File

@@ -8,7 +8,7 @@
* This file is licensed under the MIT license
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* #pw-summary Holds ProcessWire configuration settings as defined in /wire/config.php and /site/config.php.
@@ -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
@@ -98,8 +99,8 @@
*
* @property bool $advanced Special mode for ProcessWire system development. Not recommended for regular site development or production use. #pw-group-system
* @property bool $demo Special mode for demonstration use that causes POST requests to be disabled. Applies to core, but may not be safe with 3rd party modules. #pw-group-system
* @property bool|int $debug Special mode for use when debugging or developing a site. Recommended TRUE when site is in development and FALSE when not. Or set to Config::debugVerbose for verbose debug mode. #pw-group-system
* @property string $debugIf Enable debug mode if condition is met #pw-group-system
* @property bool|int|string $debug Special mode for use when debugging or developing a site. Recommended TRUE when site is in development and FALSE when not. Or set to `Config::debug*` constant. #pw-group-system
* @property string|callable|array $debugIf Enable debug mode if condition is met. One of IP address to match, regex to match IP, array of IPs to match, or callable function that returns true|false. #pw-group-system
* @property array $debugTools Tools, and their order, to show in debug mode (admin) #pw-group-system
*
* @property string $ignoreTemplateFileRegex Regular expression to ignore template files #pw-group-template-files
@@ -151,10 +152,12 @@
* @property bool $useMarkupRegions Enable support for front-end markup regions? #pw-group-system
* @property bool|array $useLazyLoading Delay loading of fields (and templates/fieldgroups) till requested? Can improve performance on systems with lots of fields or templates. #pw-group-system @since 3.0.193
* @property bool $usePageClasses Use custom Page classes in `/site/classes/[TemplateName]Page.php`? #pw-group-system @since 3.0.152
* @property bool|int|string|null $useVersionUrls Default value for $useVersion argument of $config->versionUrls() method #pw-group-system @since 3.0.227
* @property int $lazyPageChunkSize Chunk size for for $pages->findMany() calls. #pw-group-system
*
* @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
@@ -192,6 +195,25 @@
* @property int $externalPageID Page ID of page assigned to $page API variable when externally bootstrapped #pw-group-system-IDs
* @property array $preloadPageIDs Page IDs of pages that will always be preloaded at beginning of request #pw-group-system-IDs
* @property int $installed Timestamp of when this PW was installed, set automatically by the installer for future compatibility detection. #pw-group-system
*
* @method array|string wireMail($key = '', $value = null)
* @method array imageSizes($key = '', $value = null)
* @method array|bool|string|int|float imageSizerOptions($key = '', $value = null)
* @method array|int|bool webpOptions($key = '', $value = null)
* @method array|string contentTypes($key = '', $value = null)
* @method array|string fileContentTypes($key = '', $value = null)
* @method array|string|bool fileCompilerOptions($key = '', $value = null)
* @method array|string|string[] dbOptions($key = '', $value = null)
* @method array|string|string[] dbSqlModes($key = '', $value = null)
* @method array|int|bool pageList($key = '', $value = null)
* @method array|bool pageEdit($key = '', $value = null)
* @method array|string pageAdd($key = '', $value = null)
* @method array|string moduleInstall($key = '', $value = null)
* @method array|string substituteModules($key = '', $value = null)
* @method array|string|bool AdminThemeUikit($key = '', $value = null)
* @method array|string modals($key = '', $value = null)
* @method array|bool markupQA($key = '', $value = null)
* @method array|string statusFiles($key = '', $value = null)
*
*/
class Config extends WireData {
@@ -202,6 +224,12 @@ class Config extends WireData {
*/
const debugVerbose = 2;
/**
* Constant for core development debug mode (makes it use newer JS libraries in some cases)
*
*/
const debugDev = 'dev';
/**
* Get config property
*
@@ -923,10 +951,134 @@ class Config extends WireData {
*/
public function setWire(ProcessWire $wire) {
parent::setWire($wire);
$paths = $this->paths;
if($paths) $paths->setWire($wire);
$urls = $this->urls;
if($urls) $urls->setWire($wire);
foreach(array('paths', 'urls', 'styles', 'scripts') as $key) {
$value = $this->get($key);
if($value instanceof Wire) $value->setWire($wire);
}
}
}
/**
* Given array of file asset URLs return them with cache-busting version strings
*
* URLs that aready have query strings or URLs with scheme (i.e. https://) are ignored,
* except for URLs that already have a core version query string, i.e. `?v=3.0.227`
* may be converted to a different version string when appropriate.
*
* URLs that do not resolve to a physical file on the file system, relative URLs, or
* URLs that are outside of ProcessWires web root, are only eligible to receive a
* common/shared version in the URL (like the core version).
*
* To set a different default value for the `$useVersion` argument, you can populate
* the `$config->useVersionUrls` setting in your /site/config.php with the default
* value you want to substitute.
*
* ~~~~~
* foreach($config->versionUrls($config->styles) as $url) {
* echo "<link rel='stylesheet' href='$url' />";
* }
* // there is also this shortcut for the above
* foreach($config->styles->urls() as $url) {
* echo "<link rel='stylesheet' href='$url' />";
* }
* ~~~~~
*
* #pw-group-URLs
* #pw-group-tools
*
* @param array|FilenameArray|WireArray|\ArrayObject $urls Array of URLs to file assets such as JS/CSS files.
* @param bool|null|string $useVersion What to use for the version string (`null` is default):
* - `true` (bool): Get version from filemtime.
* - `false` (bool): Never get file version, just use $config->version.
* - `null` (null): Auto-detect: use file version in debug mode or dev branch only, $config->version otherwise.
* - `foobar` (string): Specify any string to be the version to use on all URLs needing it.
* `- ?foo=bar` (string): Optionally specify your own query string variable=value.
* - The default value (null) can be overridden by the `$config->useVersionUrls` setting.
* @return array Array of URLs updated with version strings where needed
* @since 3.0.227
*
*/
public function versionUrls($urls, $useVersion = null) {
$a = array();
$rootUrl = $this->urls->root;
$rootPath = $this->paths->root;
$coreVersionStr = "?v=$this->version";
if($useVersion === null) {
// if useVersion argument not specified pull from $config->useVersionUrls
$useVersion = $this->useVersionUrls;
if($useVersion === null) {
// if null or still not specified, auto-detect what to use
$useVersion = ($this->debug || ProcessWire::versionSuffix === 'dev');
}
}
if(is_string($useVersion)) {
// custom version string specified
if(!ctype_alnum(str_replace(array('.', '-', '_', '?', '='), '', $useVersion))) {
// if it fails sanitization then fallback to core version
$useVersion = false;
$versionStr = $coreVersionStr;
} else {
// use custom version str
$versionStr = $useVersion;
if(strpos($versionStr, '?') === false) $versionStr = "?v=$versionStr";
}
} else {
// use core version when appropriate
$versionStr = $coreVersionStr;
}
foreach($urls as $url) {
if(strpos($url, $coreVersionStr)) {
// url already has core version present in it
if($useVersion === false) {
// use as-is since this is already what's requested
$a[] = $url;
continue;
}
// remove existing core-version query string
list($u, $r) = explode($coreVersionStr, $url, 2);
if(!strlen($r)) $url = $u;
}
if(strpos($url, '?') !== false || strpos($url, '//') !== false) {
// leave URL with query string or scheme:// alone
$a[] = $url;
} else if($useVersion === true && strpos($url, $rootUrl) === 0) {
// use filemtime based version
$f = $rootPath . substr($url, strlen($rootUrl));
if(is_readable($f)) {
$a[] = "$url?" . base_convert((int) filemtime($f), 10, 36);
} else {
$a[] = $url . $versionStr;
}
} else {
// use standard or specified versino string
$a[] = $url . $versionStr;
}
}
return $a;
}
/**
* Given a file asset URLs return it with cache-busting version string
*
* URLs that aready have query strings are left alone.
*
* #pw-group-URLs
* #pw-group-tools
*
* @param string $url URL to a file asset (such as JS/CSS file)
* @param bool|null|string $useVersion See versionUrls() method for description of this argument.
* @return string URL updated with version strings where necessary
* @since 3.0.227
* @see Config::versionUrls()
*
*/
public function versionUrl($url, $useVersion = null) {
$a = $this->versionUrls(array($url), $useVersion);
return isset($a[0]) ? $a[0] : $url;
}
}

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

@@ -10,7 +10,7 @@
* of what other methods/objects have done to it. It also means being able
* to build a complex query without worrying about correct syntax placement.
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
* This file is licensed under the MIT license
@@ -287,7 +287,7 @@ abstract class DatabaseQuery extends WireData {
} else {
// provided key, make sure it is valid and unique (this part is not typically used)
$key = ltrim($options['key'], ':') . 'X';
if(!ctype_alnum(str_replace('_', '', $key))) $key = $this->wire('database')->escapeCol($key);
if(!ctype_alnum(str_replace('_', '', $key))) $key = $this->wire()->database->escapeCol($key);
if(empty($key) || ctype_digit($key[0]) || isset($this->bindKeys[":$key"])) {
// if key is not valid, then auto-generate one instead
unset($options['key']);
@@ -430,11 +430,12 @@ abstract class DatabaseQuery extends WireData {
* Implied parameters (using "?") was added in 3.0.157.
*
* @param string $method
* @param array $args
* @param array $arguments
* @return $this
*
*/
public function __call($method, $args) {
public function __call($method, $arguments) {
$args = &$arguments;
// if(!$this->has($method)) return parent::__call($method, $args);
if(!isset($this->queryMethods[$method])) return parent::__call($method, $args);
@@ -443,11 +444,10 @@ abstract class DatabaseQuery extends WireData {
if(!is_array($curValue)) $curValue = array();
$value = $args[0];
if(is_object($value) && $value instanceof DatabaseQuery) {
if($value instanceof DatabaseQuery) {
// if we've been given another DatabaseQuery, load from its $method
// note that if using bindValues you should also copy them separately
// behavior deprecated in 3.l0.157+, please use the copyTo() method instead
/** @var DatabaseQuery $query */
$query = $value;
$value = $query->$method; // array
if(!is_array($value) || !count($value)) return $this; // nothing to import
@@ -597,7 +597,7 @@ abstract class DatabaseQuery extends WireData {
public function getDebugQuery() {
$sql = $this->getQuery();
$suffix = $this->bindOptions['suffix'];
$database = $this->wire('database');
$database = $this->wire()->database;
foreach($this->bindValues as $bindKey => $bindValue) {
if(is_string($bindValue)) $bindValue = $database->quote($bindValue);
if($bindKey[strlen($bindKey)-1] === $suffix) {

View File

@@ -12,7 +12,7 @@
* This file is licensed under the MIT license
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
* @property array $select
@@ -25,11 +25,11 @@
* @property array $limit
* @property string $comment Comments for query
*
* @method $this select($sql, array $params = array())
* @method $this select($sql, $params = array())
* @method $this from($sql)
* @method $this join($sql, array $params = array())
* @method $this leftjoin($sql, array $params = array())
* @method $this where($sql, array $params = array())
* @method $this join($sql, $params = array())
* @method $this leftjoin($sql, $params = array())
* @method $this where($sql, $params = array())
* @method $this groupby($sql)
* @method $this limit($sql)
*
@@ -87,7 +87,7 @@ class DatabaseQuerySelect extends DatabaseQuery {
$this->getQueryMethod('limit')
) . ' ';
if($this->get('comment') && $this->wire('config')->debug) {
if($this->get('comment') && $this->wire()->config->debug) {
// NOTE: PDO thinks ? and :str param identifiers in /* comments */ are real params
// so we str_replace them out of the comment, and only support comments in debug mode
$comment = str_replace(array('*/', '?', ':'), '', $this->comment);
@@ -142,7 +142,7 @@ class DatabaseQuerySelect extends DatabaseQuery {
protected function getQuerySelect() {
if(self::$dbCache === null) {
self::$dbCache = $this->wire('config')->dbCache === false ? false : true;
self::$dbCache = $this->wire()->config->dbCache === false ? false : true;
}
$select = $this->select;

View File

@@ -20,7 +20,7 @@
* This file is licensed under the MIT license
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
* @property-read $tableField
@@ -164,19 +164,19 @@ class DatabaseQuerySelectFulltext extends Wire {
*
*/
public function __construct(DatabaseQuerySelect $query) {
parent::__construct();
$query->wire($this);
$this->query = $query;
}
/**
* @param string $key
*
* @param string $name
* @return mixed|string
*
*/
public function __get($key) {
if($key === 'tableField') return $this->tableField();
return parent::__get($key);
public function __get($name) {
if($name === 'tableField') return $this->tableField();
return parent::__get($name);
}
/**
@@ -196,7 +196,9 @@ class DatabaseQuerySelectFulltext extends Wire {
*
*/
protected function tableField() {
return "$this->tableName.$this->fieldName";
$fieldName = $this->fieldName;
if(!$fieldName) $fieldName = 'data';
return "$this->tableName.$fieldName";
}
/**
@@ -243,7 +245,7 @@ class DatabaseQuerySelectFulltext extends Wire {
*
*/
protected function escapeLike($str) {
return str_replace(array('%', '_'), array('\\%', '\\_'), $str);
return str_replace(array('%', '_'), array('\\%', '\\_'), "$str");
}
/**
@@ -256,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;
}
@@ -268,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;
@@ -296,7 +298,7 @@ class DatabaseQuerySelectFulltext extends Wire {
} else {
// disable orderby statements when calling object will be negating whatever we do
$selector = $this->query->selector;
if($selector && $selector instanceof Selector && $selector->not) $allowOrder = false;
if($selector instanceof Selector && $selector->not) $allowOrder = false;
}
// if allowOrder has not been specifically set, then set value now
@@ -361,7 +363,9 @@ class DatabaseQuerySelectFulltext extends Wire {
protected function matchArrayFieldName(array $fieldNames, $value) {
$query = $this->query;
$query->bindOption('global', true);
$this->query = $this->wire(new DatabaseQuerySelect());
$this->query = new DatabaseQuerySelect();
$this->wire($this->query);
$this->query->bindOption(true, $query->bindOption(true));
foreach($fieldNames as $fieldName) {
@@ -397,7 +401,8 @@ class DatabaseQuerySelectFulltext extends Wire {
$query = $this->query;
$query->bindOption('global', true);
$this->query = $this->wire(new DatabaseQuerySelect());
$this->query = new DatabaseQuerySelect();
$this->wire($this->query);
$this->query->bindOption(true, $query->bindOption(true));
$method = $this->method;
@@ -949,7 +954,7 @@ class DatabaseQuerySelectFulltext extends Wire {
* - `phrase` (bool): Is entire $value a full phrase to match? (default=auto-detect)
* - `useStopwords` (bool): Allow inclusion of stopwords? (default=null, auto-detect)
* - `alternates` (bool): Get word alternates? (default=null, auto-detect)
* @return string|array Value provided to the function with boolean operators added, or verbose array.
* @return array Value provided to the function with boolean operators added, or verbose array.
*
*/
protected function getBooleanModeWords($value, array $options = array()) {
@@ -1005,7 +1010,7 @@ class DatabaseQuerySelectFulltext extends Wire {
}
// iterate through all words to build boolean query values
foreach($allWords as $key => $word) {
foreach($allWords as $word) {
$length = strlen($word);
if(!$length || isset($booleanValues[$word])) continue;
@@ -1264,7 +1269,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 "));
}

View File

@@ -232,7 +232,7 @@ class Debug {
static public function getSavedTimer($key) {
$value = isset(self::$savedTimers[$key]) ? self::$savedTimers[$key] : null;
if(!is_null($value) && isset(self::$savedTimerNotes[$key])) $value = "$value - " . self::$savedTimerNotes[$key];
return $value;
return (string) $value;
}
/**
@@ -250,6 +250,29 @@ class Debug {
return $timers;
}
/**
* Remove a previously saved timer
*
* @param string $key
* @since 3.0.202
*
*/
static public function removeSavedTimer($key) {
unset(self::$savedTimers[$key]);
unset(self::$savedTimerNotes[$key]);
}
/**
* Remove all saved timers
*
* @since 3.0.202
*
*/
static public function removeSavedTimers() {
self::$savedTimers = array();
self::$savedTimerNotes = array();
}
/**
* Reset a timer so that it starts timing again from right now
*
@@ -295,6 +318,17 @@ class Debug {
* Return a backtrace array that is simpler and more PW-specific relative to PHPs debug_backtrace
*
* @param array $options
* - `limit` (int): The limit for the backtrace or 0 for no limit. (default=0)
* - `flags` (int): Flags as used by PHPs debug_backtrace() function. (default=DEBUG_BACKTRACE_PROVIDE_OBJECT)
* - `showHooks` (bool): Show inernal methods for hook calls? (default=false)
* - `getString` (bool): Get newline separated string rather than array? (default=false)
* - `getCnt` (bool): Get index number count, used for getString option only. (default=true)
* - `getFile` (bool|string): Get filename? Specify one of true, false or 'basename'. (default=true)
* - `maxCount` (int): Max size for arrays (default=10)
* - `maxStrlen` (int): Max length for strings (default=100)
* - `maxDepth` (int): Max allowed recursion depth when converting variables to strings. (default=5)
* - `ellipsis` (string): Show this ellipsis when a long value is truncated (default='…')
* - `skipCalls` (array): Method/function calls to skip.
* @return array|string
* @since 3.0.136
*
@@ -303,7 +337,7 @@ class Debug {
$defaults = array(
'limit' => 0, // the limit argument for the debug_backtrace call
'flags' => DEBUG_BACKTRACE_PROVIDE_OBJECT, // flags for PHP debug_backtrace method
'flags' => DEBUG_BACKTRACE_PROVIDE_OBJECT, // flags for PHP debug_backtrace function
'showHooks' => false, // show internal methods for hook calls?
'getString' => false, // get newline separated string rather than array?
'getCnt' => true, // get index number count (for getString only)
@@ -318,7 +352,7 @@ class Debug {
$options = array_merge($defaults, $options);
if($options['limit']) $options['limit']++;
$traces = @debug_backtrace($options['flags'], $options['limit']);
$config = wire('config');
$config = wire()->config;
$rootPath = ProcessWire::getRootPath(true);
$rootPath2 = $config && $config->paths ? $config->paths->root : $rootPath;
array_shift($traces); // shift of the simpleBacktrace call, which is not needed
@@ -331,7 +365,7 @@ class Debug {
$apiVars[wireClassName($value)] = '$' . $name;
}
foreach($traces as $n => $trace) {
foreach($traces as $trace) {
if(!is_array($trace) || !isset($trace['function']) || !isset($trace['file'])) {
continue;
@@ -342,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);
@@ -408,7 +442,7 @@ class Debug {
} else if(is_array($arg)) {
$count = count($arg);
if($count < 4) {
$arg = $count ? self::toStr($arg, array('maxDepth' => 2)) : '[]';
$arg = $count ? self::traceStr($arg, array('maxDepth' => 2)) : '[]';
} else {
$arg = 'array(' . count($arg) . ')';
}
@@ -417,8 +451,10 @@ class Debug {
$arg = '"' . $arg . '"';
} else if(is_bool($arg)) {
$arg = $arg ? 'true' : 'false';
} else if($arg === null) {
$arg = 'null';
} else {
// leave as-is
// leave as-is (int, float, etc.)
}
$newArgs[] = $arg;
}
@@ -459,7 +495,7 @@ class Debug {
* @return null|string
*
*/
static protected function toStr($value, array $options = array()) {
static protected function traceStr($value, array $options = array()) {
$defaults = array(
'maxCount' => 10, // max size for arrays
@@ -499,7 +535,11 @@ class Debug {
$suffix = $options['ellipsis'];
}
foreach($value as $k => $v) {
$value[$k] = self::toStr($v, $options);
if(is_string($k) && strlen($k)) {
$value[$k] = "$$k => " . self::traceStr($v, $options);
} else {
$value[$k] = self::traceStr($v, $options);
}
}
$str = '[ ' . implode(', ', $value) . $suffix . ' ]';
}
@@ -528,4 +568,102 @@ class Debug {
return $str;
}
/**
* Dump any variable to a debug string
*
* @param int|float|object|string|array $value
* @param array $options
* - `method` (string): Dump method to use, one of: json_encode, var_dump, var_export, print_r (default=json_encode)
* - `html` (bool): Return output-ready HTML string? (default=false)
* @return string
* @since 3.0.208
*
*/
static public function toStr($value, array $options = array()) {
$defaults = array(
'method' => 'json_encode',
'html' => false,
);
$options = array_merge($defaults, $options);
$method = $options['method'];
$prepend = '';
if(is_object($value)) {
// we format objects to arrays or strings
$className = wireClassName($value);
$classInfo = "object:$className";
$objectValue = $value;
if($objectValue instanceof \Countable) {
$classInfo .= '(' . count($objectValue) . ')';
}
if($value instanceof Wire) {
$value = $value->debugInfoSmall();
} else if(method_exists($value, '__debugInfo')) {
$value = $value->__debugInfo();
} else if(method_exists($value, '__toString')) {
$value = $classInfo . ":\"$value\"";
} else {
$value = $classInfo;
}
if(is_array($value)) {
if(empty($value)) {
$value = $classInfo;
if(method_exists($objectValue, '__toString')) {
$stringValue = (string) $objectValue;
if($stringValue != $className) $value .= ":\"$stringValue\"";
}
} else {
$prepend = "$classInfo ";
}
}
if(is_string($value)) {
$method = '';
}
} else if(is_int($value)) {
$prepend = 'int:';
} else if(is_float($value)) {
$prepend = 'float:';
} else if(is_string($value)) {
$prepend = 'string:';
} else if(is_callable($value)) {
$prepend = 'callable:';
} else if(is_resource($value)) {
$prepend = 'resource:';
}
switch($method) {
case 'json_encode':
$value = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$value = str_replace(' ', ' ', $value);
if(strpos($value, '\\"') !== false) $value = str_replace('\\"', "'", $value);
break;
case 'var_export':
$value = var_export($value, true);
break;
case 'var_dump':
ob_start();
var_dump($value);
$value = ob_get_contents();
ob_end_clean();
break;
case 'print_r':
$value = print_r($value, true);
break;
default:
$value = (string) $value;
}
if($method && $method != 'json_encode') {
// array is obvious and needs no label
if(stripos($value, 'array') === 0) $value = trim(substr($value, 5));
}
if($prepend) $value = $prepend . trim($value);
if($options['html']) $value = '<pre>' . wire()->sanitizer->entities($value) . '</pre>';
return $value;
}
}

View File

@@ -38,6 +38,7 @@
* @property array $allowContexts Names of settings that are custom configured to be allowed for context. #pw-group-properties
* @property bool|int|null $flagUnique Non-empty value indicates request for, or presence of, Field::flagUnique flag. #pw-internal
* @property Fieldgroup|null $_contextFieldgroup Fieldgroup field is in context for or null if not in context. #pw-internal
* @property true|null $distinctAutojoin When true and flagAutojoin is set, a distinct autojoin will be used. 3.0.208+ #pw-internal
*
* Common Inputfield properties that Field objects store:
* @property int|bool|null $required Whether or not this field is required during input #pw-group-properties
@@ -250,6 +251,18 @@ class Field extends WireData implements Saveable, Exportable {
*/
protected $tagList = null;
/**
* Setup name to apply when field is saved
*
* Set via $field->type = 'FieldtypeName.setupName';
* or applySetup() method
*
* @var string
* @since 3.0.213
*
*/
protected $setupName = '';
/**
* True if lowercase tables should be enforce, false if not (null = unset). Cached from $config
*
@@ -512,7 +525,14 @@ class Field extends WireData implements Saveable, Exportable {
if($this->type) {
$typeData = $this->type->exportConfigData($this, $data);
$data = array_merge($data, $typeData);
foreach($typeData as $key => $value) {
if($value === null && isset($data[$key])) {
// prevent null from overwriting non-null, alternative for #1638
unset($typeData[$key]);
}
}
// $data = array_merge($typeData, $data); // argument order reversed per #1638...
$data = array_merge($data, $typeData); // ...and later un-reversed per #1792
}
// remove named flags from data since the 'flags' property already covers them
@@ -532,7 +552,7 @@ class Field extends WireData implements Saveable, Exportable {
foreach(array('viewRoles', 'editRoles') as $roleType) {
if(!is_array($data[$roleType])) $data[$roleType] = array();
$roleNames = array();
foreach($data[$roleType] as $key => $roleID) {
foreach($data[$roleType] as $roleID) {
$role = $roles->get($roleID);
if(!$role || !$role->id) continue;
$roleNames[] = $role->name;
@@ -675,10 +695,15 @@ class Field extends WireData implements Saveable, Exportable {
*/
public function setFieldtype($type) {
if(is_object($type) && $type instanceof Fieldtype) {
if($type instanceof Fieldtype) {
// good for you
} else if(is_string($type)) {
if(strpos($type, '.')) {
// FieldtypeName.setupName
list($type, $setupName) = explode('.', $type, 2);
$this->setSetupName($setupName);
}
$typeStr = $type;
$type = $this->wire()->fieldtypes->get($type);
if(!$type) {
@@ -812,7 +837,7 @@ class Field extends WireData implements Saveable, Exportable {
$ids[] = (int) $role->id;
} else if(is_string($role) && strlen($role)) {
$rolePage = $this->wire()->roles->get($role);
if($rolePage && $rolePage->id) {
if($rolePage instanceof Role && $rolePage->id) {
$ids[] = $rolePage->id;
} else {
$this->error("Unknown role '$role'");
@@ -854,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);
}
@@ -868,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);
}
@@ -978,6 +1003,10 @@ class Field extends WireData implements Saveable, Exportable {
// predefined field settings
$inputfield->attr('name', $this->name . $contextStr);
$inputfield->set('label', $this->label);
if($contextStr) {
// keep track of original field name in Inputfields that are are renamed by context
if(!$inputfield->attr('data-field-name')) $inputfield->attr('data-field-name', $this->name);
}
// just in case an Inputfield needs to know its Fieldtype/Field context, or lack of it
$inputfield->set('hasFieldtype', $this->type);
@@ -997,7 +1026,7 @@ class Field extends WireData implements Saveable, Exportable {
}
}
if($locked && $locked === 'hidden') {
if($locked === 'hidden') {
// Inputfield should not be shown
$inputfield->collapsed = Inputfield::collapsedHidden;
} else if($locked) {
@@ -1036,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);
}
}
@@ -1098,7 +1133,8 @@ class Field extends WireData implements Saveable, Exportable {
}
if(!$fieldgroupContext || count($allowContext)) {
/** @var InputfieldWrapper $inputfields */
$inputfields = $this->wire(new InputfieldWrapper());
if(!$fieldgroupContext) $inputfields->head = $this->_('Field type details');
$inputfields->attr('title', $this->_('Details'));
@@ -1111,12 +1147,14 @@ class Field extends WireData implements Saveable, Exportable {
if(!$fieldtypeInputfields) $fieldtypeInputfields = $this->wire(new InputfieldWrapper());
$configArray = $this->type->getConfigArray($this);
if(count($configArray)) {
/** @var InputfieldWrapper $w */
$w = $this->wire(new InputfieldWrapper());
$w->importArray($configArray);
$w->populateValues($this);
$fieldtypeInputfields->import($w);
}
foreach($fieldtypeInputfields as $inputfield) {
/** @var Inputfield $inputfield */
if($fieldgroupContext && !in_array($inputfield->name, $allowContext)) continue;
$inputfields->append($inputfield);
unset($remainingNames[$inputfield->name]);
@@ -1139,6 +1177,7 @@ class Field extends WireData implements Saveable, Exportable {
if(count($inputfields)) $wrapper->append($inputfields);
}
/** @var InputfieldWrapper $inputfields */
$inputfields = $this->wire(new InputfieldWrapper());
$dummyPage = $this->wire()->pages->get('/'); // only using this to satisfy param requirement
@@ -1157,17 +1196,21 @@ class Field extends WireData implements Saveable, Exportable {
}
$inputfields->attr('title', $this->_('Input'));
$inputfields->attr('id+name', 'inputfieldConfig');
/** @var InputfieldWrapper $inputfieldInputfields */
$inputfieldInputfields = $inputfield->getConfigInputfields();
if(!$inputfieldInputfields) $inputfieldInputfields = $this->wire(new InputfieldWrapper());
if(!$inputfieldInputfields) {
/** @var InputfieldWrapper $inputfieldInputfields */
$inputfieldInputfields = $this->wire(new InputfieldWrapper());
}
$configArray = $inputfield->getConfigArray();
if(count($configArray)) {
/** @var InputfieldWrapper $w */
$w = $this->wire(new InputfieldWrapper());
$w->importArray($configArray);
$w->populateValues($this);
$inputfieldInputfields->import($w);
}
foreach($inputfieldInputfields as $i) {
/** @var Inputfield $i */
if($fieldgroupContext && !in_array($i->name, $allowContext)) continue;
$inputfields->append($i);
unset($remainingNames[$i->name]);
@@ -1205,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);
@@ -1222,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;
}
@@ -1533,6 +1579,18 @@ class Field extends WireData implements Saveable, Exportable {
return $url;
}
/**
* Set setup name from Fieldtype to apply when field is saved
*
* @param string $setupName Setup name or omit to instead get the current value
* @return string Returns current value
*
*/
public function setSetupName($setupName = null) {
if($setupName !== null) $this->setupName = $setupName;
return $this->setupName;
}
/**
* debugInfo PHP 5.6+ magic method
*
@@ -1566,4 +1624,3 @@ class Field extends WireData implements Saveable, Exportable {
}
}

View File

@@ -13,7 +13,7 @@
* 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 2023 by Ryan Cramer
* https://processwire.com
*
*/
@@ -48,6 +48,7 @@ class FieldSelectorInfo extends Wire {
*
*/
public function __construct() {
parent::__construct();
$ftNops = array();
$ftOps = Selectors::getOperators(array(
@@ -81,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

@@ -65,7 +65,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
*/
public function isValidItem($item) {
return is_object($item) && $item instanceof Field;
return $item instanceof Field;
}
/**
@@ -87,10 +87,11 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
* #pw-internal
*
* @param $item
* @return int|string
* @return int
*
*/
public function getItemKey($item) {
/** @var Field $item */
return $item->id;
}
@@ -116,15 +117,16 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
* #pw-group-manipulation
*
* @param Field|string $field Field object, field name or id.
* @param Field|string $item Field object, field name or id.
* @return $this
* @throws WireException
*
*/
public function add($field) {
public function add($item) {
$field = $item;
if(!is_object($field)) $field = $this->wire()->fields->get($field);
if($field && $field instanceof Field) {
if($field instanceof Field) {
if(!$field->id) {
throw new WireException("You must save field '$field' before adding to Fieldgroup '$this->name'");
}
@@ -149,13 +151,14 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
* #pw-group-manipulation
*
* @param Field|string $field Field object or field name, or id.
* @param Field|string $key Field object or field name, or id.
* @return bool True on success, false on failure.
*
*/
public function remove($field) {
if(!is_object($field)) $field = $this->wire('fields')->get($field);
public function remove($key) {
$field = $key;
if(!is_object($field)) $field = $this->wire()->fields->get($field);
if(!$this->getField($field->id)) return false;
if(!$field) return true;
@@ -205,7 +208,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*/
public function softRemove($field) {
if(!is_object($field)) $field = $this->wire('fields')->get($field);
if(!is_object($field)) $field = $this->wire()->fields->get($field);
if(!$this->getField($field->id)) return false;
if(!$field) return true;
@@ -238,7 +241,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
*/
public function getField($key, $useFieldgroupContext = false) {
if(is_object($key) && $key instanceof Field) $key = $key->id;
if($key instanceof Field) $key = $key->id;
if(is_string($key) && ctype_digit("$key")) $key = (int) $key;
if($this->isValidKey($key)) {
@@ -248,6 +251,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
$value = null;
foreach($this as $field) {
/** @var Field $field */
if($field->name == $key) {
$value = $field;
break;
@@ -291,9 +295,9 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
*/
public function hasFieldContext($field, $namespace = '') {
if(is_object($field) && $field instanceof Field) $field = $field->id;
if($field instanceof Field) $field = $field->id;
if(is_string($field) && !ctype_digit($field)) {
$field = $this->wire('fields')->get($field);
$field = $this->wire()->fields->get($field);
$field = $field && $field->id ? $field->id : 0;
}
if(isset($this->fieldContexts[(int) $field])) {
@@ -349,7 +353,10 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
if($key == 'fields') return $this;
if($key == 'fields_id') {
$values = array();
foreach($this as $field) $values[] = $field->id;
foreach($this as $field) {
/** @var Field $field */
$values[] = $field->id;
}
return $values;
}
if($key == 'removedFields') return $this->removedFields;
@@ -429,7 +436,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
*/
public function save() {
$this->wire('fieldgroups')->save($this);
$this->wire()->fieldgroups->save($this);
return $this;
}
@@ -460,8 +467,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
*/
public function getExportData() {
/** @var Fieldgroups $fieldgroups */
$fieldgroups = $this->wire('fieldgroups');
$fieldgroups = $this->wire()->fieldgroups;
return $fieldgroups->getExportData($this);
}
@@ -506,7 +512,13 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
* @param Page $page Page that the Inputfields will be for.
* @param string|array $contextStr Optional context string to append to all the Inputfield names, OR array of options.
* - Optional context string is helpful for things like repeaters.
* - You may instead specify associative array of any method arguments if preferred.
* - Or associative array with any of these options:
* - `contextStr` (string): Context string to append to all Inputfield names.
* - `fieldName` (string|array): Limit to particular fieldName(s) or field ID(s). See $fieldName argument for details.
* - `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.
@@ -524,16 +536,26 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
'fieldName' => $fieldName,
'namespace' => $namespace,
'flat' => $flat,
'populate' => true, // populate page values?
'container' => null,
);
$options = $contextStr;
$options = array_merge($defaults, $options);
$contextStr = $options['contextStr'];
$fieldName = $options['fieldName'];
$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;
@@ -562,6 +584,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
}
foreach($this as $field) {
/** @var Field $field */
// for named multi-field retrieval
if($multiMode && !isset($fieldInputfields[$field->id])) continue;
@@ -612,7 +635,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
continue;
}
} else if($field->modal && $field->type instanceof FieldtypeFieldsetOpen) {
} else if($field->get('modal') && $field->type instanceof FieldtypeFieldsetOpen) {
// field requires modal
$inModalGroup = $field->name;
@@ -629,6 +652,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
// start a new container
$inputfield = $field->getInputfield($page, $contextStr);
if(!$inputfield) $inputfield = $this->wire(new InputfieldWrapper());
/** @var Inputfield|InputfieldWrapper $inputfield */
if($inputfield->collapsed == Inputfield::collapsedHidden) continue;
$container->add($inputfield);
$containers[] = $container;
@@ -641,7 +665,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
if(!$inputfield) continue;
if($inputfield->collapsed == Inputfield::collapsedHidden) continue;
if(!$page instanceof NullPage) {
if($populate && !$page instanceof NullPage) {
$value = $page->get($field->name);
$inputfield->setAttribute('value', $value);
}
@@ -655,7 +679,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
if($multiMode) {
// add to container in requested order
foreach($fieldInputfields as $fieldID => $inputfield) {
foreach($fieldInputfields as /* $fieldID => */ $inputfield) {
if($inputfield) $container->add($inputfield);
}
}
@@ -686,7 +710,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
*/
public function getNumTemplates() {
return $this->wire('fieldgroups')->getNumTemplates($this);
return $this->wire()->fieldgroups->getNumTemplates($this);
}
/**
@@ -756,9 +780,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
*/
public function saveContext() {
return $this->wire('fieldgroups')->saveContext($this);
return $this->wire()->fieldgroups->saveContext($this);
}
}

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 = '')
@@ -136,7 +136,7 @@ class Fieldgroups extends WireSaveableItemsLookup {
$templates = $this->wire()->templates;
$num = 0;
foreach($templates->getAllValues('fieldgroups_id', 'id') as $templateId => $fieldgroupId) {
foreach($templates->getAllValues('fieldgroups_id', 'id') as /* $templateId => */ $fieldgroupId) {
if($fieldgroupId == $fieldgroup->id) $num++;
}
@@ -180,6 +180,7 @@ class Fieldgroups extends WireSaveableItemsLookup {
if(!$useLazy && !is_object($fieldgroup)) $fieldgroup = $this->get($fieldgroup);
if($fieldgroup instanceof Fieldgroup) {
foreach($fieldgroup as $field) {
/** @var Field $field */
$fieldNames[$field->id] = $field->name;
}
return $fieldNames;
@@ -231,10 +232,13 @@ class Fieldgroups extends WireSaveableItemsLookup {
foreach($this->wire()->templates as $template) {
if($template->fieldgroup->id !== $fieldgroup->id) continue;
foreach($fieldgroup->removedFields as $field) {
/** @var Field $field */
// make sure the field is valid to delete from this template
$error = $this->isFieldNotRemoveable($field, $fieldgroup, $template);
if($error !== false) throw new WireException("$error Save of fieldgroup changes aborted.");
if($field->type) $field->type->deleteTemplateField($template, $field);
/** @var Fieldtype $fieldtype */
$fieldtype = $field->type;
if($fieldtype) $fieldtype->deleteTemplateField($template, $field);
$fieldgroup->finishRemove($field);
$fieldsRemoved[] = $field;
}
@@ -308,7 +312,8 @@ class Fieldgroups extends WireSaveableItemsLookup {
public function ___delete(Saveable $item) {
$templates = array();
foreach($this->wire('templates') as $template) {
foreach($this->wire()->templates as $template) {
/** @var Template $template */
if($template->fieldgroup->id == $item->id) $templates[] = $template->name;
}
@@ -330,7 +335,7 @@ class Fieldgroups extends WireSaveableItemsLookup {
*
*/
public function deleteField(Field $field) {
$database = $this->wire('database');
$database = $this->wire()->database;
$query = $database->prepare("DELETE FROM fieldgroups_fields WHERE fields_id=:fields_id"); // QA
$query->bindValue(":fields_id", $field->id, \PDO::PARAM_INT);
$result = $query->execute();
@@ -344,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;
}
/**
@@ -389,6 +421,7 @@ class Fieldgroups extends WireSaveableItemsLookup {
$fields = array();
$contexts = array();
foreach($fieldgroup as $field) {
/** @var Field $field */
$fields[] = $field->name;
$fieldContexts = $fieldgroup->getFieldContextArray();
if(isset($fieldContexts[$field->id])) {
@@ -450,18 +483,19 @@ class Fieldgroups extends WireSaveableItemsLookup {
// figure out which fields should be removed
foreach($fieldgroup as $field) {
/** @var Field $field */
$fieldNames[$field->name] = $field->name;
if(!in_array($field->name, $data['fields'])) {
$fieldgroup->remove($field);
$label = "-$field->name";
$return['fields']['new'] .= $label . "\n";;
$return['fields']['new'] .= $label . "\n";
$rmFields[] = $field->name;
}
}
// figure out which fields should be added
foreach($data['fields'] as $name) {
$field = $this->wire('fields')->get($name);
$field = $this->wire()->fields->get($name);
if(in_array($name, $rmFields)) continue;
if(!$field) {
$error = sprintf($this->_('Unable to find field: %s'), $name);
@@ -559,14 +593,14 @@ 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);
if(is_null($template)) $template = $this->wire()->templates->get($fieldgroup->name);
if(($field->flags & Field::flagGlobal) && (!$template || !$template->noGlobal)) {
if($template && $template->getConnectedField()) {
@@ -612,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 2022 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.
@@ -27,6 +27,7 @@
* @method void changeTypeReady(Saveable $item, Fieldtype $fromType, Fieldtype $toType) #pw-hooker
* @method bool|Field clone(Field $item, $name = '') Clone a field and return it or return false on fail.
* @method array getTags($getFieldNames = false) Get tags for all fields (3.0.179+) #pw-advanced
* @method bool applySetupName(Field $field, $setupName = '')
*
*/
@@ -106,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)
*
@@ -156,6 +164,10 @@ class Fields extends WireSaveableItems {
self::$nativeNamesSystem = array_flip(self::$nativeNamesSystem);
}
}
public function getCacheItemName() {
return array('roles', 'permissions', 'title', 'process');
}
/**
* Construct and load the Fields
@@ -179,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
*
@@ -245,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;
@@ -312,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
*
@@ -332,8 +388,14 @@ class Fields extends WireSaveableItems {
// even if only the case has changed.
$schema = $item->type->getDatabaseSchema($item);
if(!empty($schema)) {
$database->exec("RENAME TABLE `$prevTable` TO `tmp_$table`"); // QA
$database->exec("RENAME TABLE `tmp_$table` TO `$table`"); // QA
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 `$tmpTable`"); // QA
$database->exec("RENAME TABLE `$tmpTable` TO `$table`"); // QA
}
$item->prevTable = '';
}
@@ -353,9 +415,18 @@ class Fields extends WireSaveableItems {
}
if(!$item->type) throw new WireException("Can't save a Field that doesn't have it's 'type' property set to a Fieldtype");
$item->type->saveFieldReady($item);
if(!parent::___save($item)) return false;
if($isNew) $item->type->createField($item);
$setupName = $item->setSetupName();
if($setupName || $isNew) {
if($this->applySetupName($item, $setupName)) {
$item->setSetupName('');
parent::___save($item);
}
}
if($item->flags & Field::flagGlobal) {
// make sure that all template fieldgroups contain this field and add to any that don't.
foreach($this->wire()->templates as $template) {
@@ -414,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
*
@@ -455,9 +526,9 @@ 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 bool|Saveable $item Returns the new clone on success, or false on failure
* @return Field $item Returns the new clone on success, or false on failure
*
*/
public function ___clone(Saveable $item, $name = '') {
@@ -637,9 +708,10 @@ class Fields extends WireSaveableItems {
$flags = $field2->flags;
if($flags & Field::flagSystem) {
$field2->flags = $flags | Field::flagSystemOverride;
$field2->flags = 0;
$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;
@@ -688,7 +760,7 @@ class Fields extends WireSaveableItems {
$this->error("Field type change failed. Database reports: $error");
$database->exec("DROP TABLE `$table2`"); // QA
$severe = $this->wire()->process != 'ProcessField';
if($exception) $this->trackException($exception, $severe);
$this->trackException($exception, $severe);
return false;
}
@@ -769,7 +841,7 @@ class Fields extends WireSaveableItems {
// so use verbose/slow method to delete the field from pages
$ids = $this->getNumPages($field, array('template' => $template, 'getPageIDs' => true));
$items = $this->wire('pages')->getById($ids, $template);
$items = $this->wire()->pages->getById($ids, $template);
foreach($items as $page) {
try {
@@ -787,7 +859,7 @@ class Fields extends WireSaveableItems {
// large number of pages to operate on: use fast method
$database = $this->wire('database');
$database = $this->wire()->database;
$table = $database->escapeTable($field->getTable());
$sql = "DELETE $table FROM $table " .
"INNER JOIN pages ON pages.id=$table.pages_id " .
@@ -1074,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
*
@@ -1112,6 +1208,7 @@ class Fields extends WireSaveableItems {
}
foreach($this->getWireArray() as $field) {
/** @var Field $field */
$fieldtype = $field->type;
if(!$fieldtype) continue;
@@ -1219,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)
@@ -1227,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"');
}
@@ -1265,7 +1362,7 @@ class Fields extends WireSaveableItems {
*
* #pw-hooker
*
* @param Field|Saveable $item
* @param Field $item
* @param Fieldtype $fromType
* @param Fieldtype $toType
*
@@ -1277,7 +1374,7 @@ class Fields extends WireSaveableItems {
*
* #pw-hooker
*
* @param Field|Saveable $item
* @param Field $item
* @param Fieldtype $fromType
* @param Fieldtype $toType
*
@@ -1290,7 +1387,7 @@ class Fields extends WireSaveableItems {
* #pw-internal
*
* @param Field $field
* @return array Array of Fieldtype objects indexed by class name
* @return Fieldtypes
* @since 3.0.140
*
*/
@@ -1298,15 +1395,16 @@ class Fields extends WireSaveableItems {
$fieldtype = $field->type;
if($fieldtype) {
// ask fieldtype what is compatible
/** @var Fieldtypes $fieldtypes */
$fieldtypes = $fieldtype->getCompatibleFieldtypes($field);
if(!$fieldtypes || !$fieldtypes instanceof WireArray) {
if(!$fieldtypes instanceof WireArray) {
$fieldtypes = $this->wire(new Fieldtypes());
}
// ensure original is present
$fieldtypes->prepend($fieldtype);
} else {
// allow all
$fieldtypes = $this->wire('fieldtypes');
$fieldtypes = $this->wire()->fieldtypes;
}
return $fieldtypes;
}
@@ -1339,7 +1437,8 @@ class Fields extends WireSaveableItems {
$fieldId = $this->_fieldId($field);
$fieldgroups = $this->wire()->fieldgroups;
$items = $getCount ? null : $this->wire(new FieldgroupsArray()); /** @var FieldgroupsArray $items */
/** @var FieldgroupsArray $items */
$items = $getCount ? null : $this->wire(new FieldgroupsArray());
$ids = array();
$count = 0;
@@ -1413,6 +1512,41 @@ class Fields extends WireSaveableItems {
return $getCount ? $count : $items;
}
/**
* Setup a new field using predefined setup name(s) from the Fields fieldtype
*
* If no setupName is provided then this method doesnt do anything, but hooks to it might.
*
* @param Field $field Newly created field
* @param string $setupName Setup name to apply
* @return bool True if setup was appled, false if not
* @since 3.0.213
*
*/
protected function ___applySetupName(Field $field, $setupName = '') {
$setups = $field->type->getFieldSetups();
$setup = isset($setups[$setupName]) ? $setups[$setupName] : null;
if(!$setup) return false;
$title = isset($setup['title']) ? $setup['title'] : $setupName;
$func = isset($setup['setup']) ? $setup['setup'] : null;
foreach($setup as $property => $value) {
if($property === 'title' || $property === 'setup') continue;
$field->set($property, $value);
}
if($func && is_callable($func)) {
$func($field);
}
$this->message("Applied setup: $title", Notice::debug | Notice::noGroup);
return true;
}
/**
* Return field ID for given value (Field, field name, field ID) or 0 if none
*
@@ -1436,4 +1570,3 @@ class Fields extends WireSaveableItems {
}
}

View File

@@ -5,7 +5,7 @@
*
* WireArray of Field instances, as used by Fields class
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
@@ -27,7 +27,7 @@ class FieldsArray extends WireArray {
* Per WireArray interface, Field keys have to be integers
*
* @param int $key
* @return int
* @return bool
*
*/
public function isValidKey($key) {
@@ -54,4 +54,4 @@ class FieldsArray extends WireArray {
public function makeBlankItem() {
return $this->wire(new Field());
}
}
}

View File

@@ -4,7 +4,7 @@
*
* #pw-summary Methods for managing DB tables and indexes for fields, and related methods. Accessed from `$fields->tableTools()`.
*
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @since 3.0.150
@@ -39,8 +39,7 @@ class FieldsTableTools extends Wire {
$options = array_merge($defaults, $options);
$result = array();
/** @var WireDatabasePDO $database */
$database = $this->wire('database');
$database = $this->wire()->database;
$table = $database->escapeTable($field->getTable());
$col = $database->escapeCol($options['column']);
$sql = "SELECT $col, COUNT($col) FROM $table ";
@@ -107,8 +106,7 @@ class FieldsTableTools extends Wire {
*/
public function setUniqueIndex(Field $field, $add = true) {
/** @var WireDatabasePDO $database */
$database = $this->wire('database');
$database = $this->wire()->database;
$col = 'data';
$table = $database->escapeTable($field->getTable());
$uniqueIndexName = $this->hasUniqueIndex($field, $col);
@@ -192,8 +190,7 @@ class FieldsTableTools extends Wire {
*
*/
public function hasUniqueIndex(Field $field, $col = 'data') {
/** @var WireDatabasePDO $database */
$database = $this->wire('database');
$database = $this->wire()->database;
$table = $database->escapeTable($field->getTable());
$sql = "SHOW INDEX FROM $table";
$query = $database->prepare($sql);
@@ -222,6 +219,8 @@ class FieldsTableTools extends Wire {
static $checking = false;
if($checking) return;
$database = $this->wire()->database;
$col = 'data';
// is unique index requested?
@@ -232,7 +231,7 @@ class FieldsTableTools extends Wire {
if($useUnique === $hasUnique) return;
if(!$this->database->tableExists($field->getTable())) return;
if(!$database->tableExists($field->getTable())) return;
$checking = true;
@@ -286,8 +285,7 @@ class FieldsTableTools extends Wire {
*/
public function deleteEmptyRows(Field $field, $col = 'data', $strict = true) {
/** @var WireDatabasePDO $database */
$database = $this->wire('database');
$database = $this->wire()->database;
$table = $database->escapeTable($field->getTable());
$fieldtype = $field->type;
$schema = $fieldtype->getDatabaseSchema($field);
@@ -314,7 +312,7 @@ class FieldsTableTools extends Wire {
if(!in_array(trim($type), $types)) return false; // if not in allowed col types, fail
if($col !== 'data') {
$col = $database->escapeCol($this->sanitizer->fieldName($col));
$col = $database->escapeCol($this->wire()->sanitizer->fieldName($col));
if(empty($col)) return false;
}
@@ -354,7 +352,7 @@ class FieldsTableTools extends Wire {
public function getUniqueIndexInputfield(Field $field) {
$col = 'data';
$modules = $this->wire('modules'); /** @var Modules $modules */
$modules = $this->wire()->modules;
if((bool) $field->flagUnique != $field->hasFlag(Field::flagUnique)) {
$this->checkUniqueIndex($field, true);
@@ -386,10 +384,9 @@ class FieldsTableTools extends Wire {
*
*/
public function valueExists(Field $field, $value, $col = 'data') {
/** @var WireDatabasePDO $database */
$database = $this->wire('database');
$database = $this->wire()->database;
$table = $database->escapeTable($field->getTable());
if($col !== 'data') $col = $database->escapeCol($this->sanitizer->fieldName($col));
if($col !== 'data') $col = $database->escapeCol($this->wire()->sanitizer->fieldName($col));
$sql = "SELECT pages_id FROM $table WHERE $col=:val LIMIT 1";
$query = $database->prepare($sql);
$query->bindValue(':val', $value);
@@ -399,4 +396,4 @@ class FieldsTableTools extends Wire {
return $pageId;
}
}
}

View File

@@ -15,7 +15,7 @@
* of most other methods provided in this Fieldtype class accounts for most situations already.
* #pw-body
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
*
@@ -47,8 +47,10 @@
* @method Field cloneField(Field $field)
* @method void renamedField(Field $field, $prevName)
* @method void savedField(Field $field)
* @method void saveFieldReady(Field $field)
* @method void install()
* @method void uninstall()
* @method array getFieldSetups()
*
* @property bool $_exportMode True when Fieldtype is exporting config data, false otherwise. #pw-internal
* @property string $name Name of Fieldtype module. #pw-group-other
@@ -86,12 +88,6 @@ abstract class Fieldtype extends WireData implements Module {
*/
protected $lastAccessField = null;
/**
* Construct
*
*/
public function __construct() { }
/**
* Per Module interface, this template method is called when all system classes are loaded and ready for API usage
*
@@ -175,13 +171,47 @@ abstract class Fieldtype extends WireData implements Module {
// in Inputfield modules.
//
// (!) See `FieldtypeFile` for an example that uses both Page and Field params.
if($page && $field) {}
$inputfield = $this->wire('modules')->get('InputfieldText');
/** @var Inputfield $inputfield */
$inputfield = $this->wire()->modules->get('InputfieldText');
$inputfield->class = $this->className();
return $inputfield;
}
/**
* Get predefined setups for newly created fields of this type
*
* ~~~~~
* // Example that returns 2 setup options "foo" and "bar"
* return array(
* 'foo' => array(
* 'title' => 'Foo',
* 'any_setting' => 'any_value',
* 'setup' => function(Field $field) {
* // optional code to setup $field when 'foo' is selected
* }
* ),
* 'bar' => array(
* 'title' => 'Bar',
* 'hello' => 'world', // example of any setting
* 'setup' => function(Field $field) {
* // optional code to setup $field when 'bar' is selected
* }
* )
* );
* ~~~~~
*
* #pw-internal
* #pw-hooker
*
* @return array
* @since 3.0.213
*
*/
public function ___getFieldSetups() {
return array();
}
/**
* Get any Inputfields used for configuration of this Fieldtype.
*
@@ -200,7 +230,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function ___getConfigInputfields(Field $field) {
if($field) {}
$inputfields = $this->wire(new InputfieldWrapper());
/*
@@ -231,7 +260,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function ___getConfigArray(Field $field) {
if($field) {}
return array();
}
@@ -251,7 +279,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function ___getConfigAllowContext(Field $field) {
if($field) {}
return array();
}
@@ -276,10 +303,12 @@ abstract class Fieldtype extends WireData implements Module {
// @TODO should this be moved back into modules/Process/ProcessField.module? (that's where it is saved)
// advanced settings
$inputfields = $this->wire(new InputfieldWrapper());
if($this->getLoadQueryAutojoin($field, $this->wire(new DatabaseQuerySelect()))) {
$modules = $this->wire()->modules;
$dqs = $this->wire(new DatabaseQuerySelect()); /** @var DatabaseQuerySelect $dqs */
if($this->getLoadQueryAutojoin($field, $dqs)) {
/** @var InputfieldCheckbox $f */
$f = $this->modules->get('InputfieldCheckbox');
$f = $modules->get('InputfieldCheckbox');
$f->label = $this->_('Autojoin');
$f->icon = 'sign-in';
$f->attr('name', 'autojoin');
@@ -289,7 +318,8 @@ abstract class Fieldtype extends WireData implements Module {
$inputfields->append($f);
}
$f = $this->modules->get('InputfieldCheckbox');
/** @var InputfieldCheckbox $f */
$f = $modules->get('InputfieldCheckbox');
$f->attr('name', 'global');
$f->label = $this->_('Global');
$f->icon = 'globe';
@@ -302,23 +332,31 @@ abstract class Fieldtype extends WireData implements Module {
}
$inputfields->append($f);
if($this->config->advanced) {
$f = $this->modules->get('InputfieldCheckbox');
if($this->wire()->config->advanced) {
/** @var InputfieldCheckbox $f */
$f = $modules->get('InputfieldCheckbox');
$f->attr('name', 'system');
$f->label = 'System';
$f->description = "If checked, this field is considered a system field and is not renameable or deleteable. System fields may not be undone using ProcessWire's API.";
$f->attr('value', 1);
if($field->flags & Field::flagSystem) $f->attr('checked', 'checked');
else $f->collapsed = true;
if($field->flags & Field::flagSystem) {
$f->attr('checked', 'checked');
} else {
$f->collapsed = true;
}
$inputfields->append($f);
$f = $this->modules->get('InputfieldCheckbox');
/** @var InputfieldCheckbox $f */
$f = $modules->get('InputfieldCheckbox');
$f->attr('name', 'permanent');
$f->label = 'Permanent';
$f->description = "If checked, this field is considered a permanent field and it can't be removed from any of the system templates/fieldgroups to which it is attached. This flag may not be undone using ProcessWire's API.";
$f->attr('value', 1);
if($field->flags & Field::flagPermanent) $f->attr('checked', 'checked');
else $f->collapsed = true;
if($field->flags & Field::flagPermanent) {
$f->attr('checked', 'checked');
} else {
$f->collapsed = true;
}
$inputfields->append($f);
}
@@ -399,9 +437,8 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function ___getCompatibleFieldtypes(Field $field) {
if($field) {}
$fieldtypes = $this->wire(new Fieldtypes());
foreach($this->wire('fieldtypes') as $fieldtype) {
foreach($this->wire()->fieldtypes as $fieldtype) {
if(!$fieldtype instanceof FieldtypeMulti) $fieldtypes->add($fieldtype);
}
return $fieldtypes;
@@ -441,7 +478,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function ___formatValue(Page $page, Field $field, $value) {
if($page && $field) {}
return $value;
}
@@ -474,6 +510,7 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function ___markupValue(Page $page, Field $field, $value = null, $property = '') {
/** @var MarkupFieldtype $m */
$m = $this->wire(new MarkupFieldtype($page, $field, $value));
if(strlen($property)) return $m->render($property);
return $m;
@@ -492,7 +529,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function getBlankValue(Page $page, Field $field) {
if($page && $field) {}
return '';
}
@@ -541,7 +577,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function isEmptyValue(Field $field, $value) {
if($field) {}
return empty($value);
}
@@ -564,7 +599,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function ___wakeupValue(Page $page, Field $field, $value) {
if($page && $field) {}
return $value;
}
@@ -587,7 +621,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function ___sleepValue(Page $page, Field $field, $value) {
if($page && $field) {}
return $value;
}
@@ -608,7 +641,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function ___importValue(Page $page, Field $field, $value, array $options = array()) {
if($options) {}
$value = $this->wakeupValue($page, $field, $value);
return $value;
}
@@ -673,7 +705,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function ___exportValue(Page $page, Field $field, $value, array $options = array()) {
if($options) {}
$value = $this->sleepValue($page, $field, $value);
return $value;
}
@@ -687,7 +718,7 @@ abstract class Fieldtype extends WireData implements Module {
*
* @param Page $page
* @param Field $field
* @return mixed
* @return mixed
*
*/
public function getDefaultValue(Page $page, Field $field) {
@@ -695,7 +726,9 @@ abstract class Fieldtype extends WireData implements Module {
$value = $field->getDefaultValue();
if(!is_null($value)) return $value;
*/
return $this->getBlankValue($page, $field);
/** @var mixed $value */
$value = $this->getBlankValue($page, $field);
return $value;
}
/**
@@ -733,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 WireException("Operator '$operator' is not implemented in $this->className");
}
$table = $database->escapeTable($table);
@@ -772,8 +805,9 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function getMatchQuerySort(Field $field, $query, $table, $subfield, $desc) {
if($query && $table && $field && $subfield && $desc) {}
return false;
/** @var bool|string $value */
$value = false;
return $value;
}
/**
@@ -804,7 +838,7 @@ abstract class Fieldtype extends WireData implements Module {
*/
public function ___createField(Field $field) {
$database = $this->wire('database');
$database = $this->wire()->database;
$schema = $this->getDatabaseSchema($field);
if(!isset($schema['pages_id'])) throw new WireException("Field '$field' database schema must have a 'pages_id' field.");
@@ -878,9 +912,9 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function getDatabaseSchema(Field $field) {
if($field) {}
$engine = $this->wire('config')->dbEngine;
$charset = $this->wire('config')->dbCharset;
$config = $this->wire()->config;
$engine = $config->dbEngine;
$charset = $config->dbCharset;
$schema = array(
'pages_id' => 'int UNSIGNED NOT NULL',
'data' => "int NOT NULL", // each Fieldtype should override this in particular
@@ -912,7 +946,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function getFieldClass(array $a = array()) {
if($a) {} // ignore
return '';
}
@@ -995,8 +1028,9 @@ abstract class Fieldtype extends WireData implements Module {
if($a == 'CHARSET') $info['charset'] = $b;
}
}
if(!$info['engine']) $info['engine'] = $this->wire('config')->dbEngine;
if(!$info['charset']) $info['charset'] = $this->wire('config')->dbCharset;
$config = $this->wire()->config;
if(!$info['engine']) $info['engine'] = $config->dbEngine;
if(!$info['charset']) $info['charset'] = $config->dbCharset;
if($info['engine']) $info['engine'] = str_replace(array('MYISAM', 'INNODB'), array('MyISAM', 'InnoDB'), $info['engine']);
$info['transactions'] = $info['engine'] == 'InnoDB';
}
@@ -1088,7 +1122,7 @@ abstract class Fieldtype extends WireData implements Module {
*/
public function ___getSelectorInfo(Field $field, array $data = array()) {
if($data) {}
$selectorInfo = $this->wire(new FieldSelectorInfo());
$selectorInfo = $this->wire(new FieldSelectorInfo()); /** @var FieldSelectorInfo $selectorInfo */
return $selectorInfo->getSelectorInfo($field);
}
@@ -1112,11 +1146,9 @@ abstract class Fieldtype extends WireData implements Module {
if(!$page->id || !$field->id) return null;
/** @var WireDatabasePDO $database */
$database = $this->wire('database');
$database = $this->wire()->database;
$schema = $this->getDatabaseSchema($field);
$table = $database->escapeTable($field->table);
$value = null;
$stmt = null;
/** @var DatabaseQuerySelect $query */
@@ -1134,16 +1166,17 @@ abstract class Fieldtype extends WireData implements Module {
$this->trackException($e, false, true);
}
if(!$result) return $value;
if(!$result) return null;
$fieldName = $database->escapeCol($field->name);
$schema = $this->trimDatabaseSchema($schema);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
$stmt->closeCursor();
if(!$row) return $value;
if(!$row) return null;
$value = array();
foreach($schema as $k => $unused) {
// properties from DB are always as "fieldName__column", example "title__data" (see getLoadQuery)
$key = $fieldName . '__' . $k;
@@ -1170,11 +1203,9 @@ abstract class Fieldtype extends WireData implements Module {
* @param Field $field Field to retrieve from the page.
* @param Selectors|string|array $selector
* @return mixed|null
* @throws WireException
*
*/
public function ___loadPageFieldFilter(Page $page, Field $field, $selector) {
if(false) throw new WireException(); // a gift for the ide
$this->setLoadPageFieldFilters($field, $selector);
$value = $this->loadPageField($page, $field);
$this->setLoadPageFieldFilters($field, null);
@@ -1192,7 +1223,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function setLoadPageFieldFilters(Field $field, $selectors) {
if($field) {}
if(empty($selectors)) {
$this->loadPageFieldFilters = null;
} else if($selectors instanceof Selectors) {
@@ -1213,7 +1243,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function getLoadPageFieldFilters(Field $field) {
if($field) {}
return $this->loadPageFieldFilters;
}
@@ -1229,7 +1258,7 @@ abstract class Fieldtype extends WireData implements Module {
*/
public function getLoadQuery(Field $field, DatabaseQuerySelect $query) {
$database = $this->wire('database');
$database = $this->wire()->database;
$table = $database->escapeTable($field->table);
$schema = $this->trimDatabaseSchema($this->getDatabaseSchema($field));
$fieldName = $database->escapeCol($field->name);
@@ -1271,13 +1300,16 @@ abstract class Fieldtype extends WireData implements Module {
*/
public function ___savePageField(Page $page, Field $field) {
if(!$page->id) throw new WireException("Unable to save to '{$field->table}' for page that doesn't exist in pages table");
if(!$field->id) throw new WireException("Unable to save to '{$field->table}' for field that doesn't exist in fields table");
if(!$page->id || !$field->id) {
$t = $field->id ? 'pages' : 'fields';
$message = "Unable to save to '$field->table' for page that doesn't exist in $t table";
throw new WireException($message);
}
// if this field hasn't changed since it was loaded, don't bother executing the save
if(!$page->isChanged($field->name)) return true;
$database = $this->wire('database');
$database = $this->wire()->database;
$value = $page->get($field->name);
// if the value is one that should be deleted, then remove the field from the database because it's redundant
@@ -1347,7 +1379,10 @@ abstract class Fieldtype extends WireData implements Module {
} catch(\PDOException $e) {
if($e->getCode() == 23000) {
$message = sprintf($this->_('Value not allowed for field “%s” because it is already in use'), $field->name);
$message = sprintf(
$this->_('Value not allowed for field “%s” because it is already in use'),
$field->name
);
throw new WireDatabaseException($message, $e->getCode(), $e);
} else {
throw $e;
@@ -1372,7 +1407,7 @@ abstract class Fieldtype extends WireData implements Module {
*/
public function ___deleteField(Field $field) {
try {
$database = $this->wire('database');
$database = $this->wire()->database;
$table = $database->escapeTable($field->table);
$query = $database->prepare("DROP TABLE `$table`"); // QA
$result = $query->execute();
@@ -1507,13 +1542,28 @@ abstract class Fieldtype extends WireData implements Module {
* Most Fieldtypes don't need to do anything here, but this exists just in case.
*
* #pw-internal
* #pw-hooker
*
* @param Field $field
* @param string $prevName Previous name (current name can be found in $field->name)
*
*/
public function ___renamedField(Field $field, $prevName) {
if($field && $prevName) {}
}
/**
* Hook called by Fields::save() when a field is about to be saved
*
* If field is a new field it will not yet have an id.
*
* #pw-internal
* #pw-hooker
*
* @param Field $field
* @since 3.0.212
*
*/
public function ___saveFieldReady(Field $field) {
}
/**
@@ -1522,6 +1572,9 @@ abstract class Fieldtype extends WireData implements Module {
* This is primarily so that Fieldtype modules can identify when their fields are
* saved without having to add a hook to the $fields API var.
*
* #pw-internal
* #pw-hooker
*
* @param Field $field
* @since 3.0.171
*
@@ -1539,12 +1592,13 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function get($key) {
if($key == 'name') return $this->className();
if($key == 'shortName') {
if($key === 'name') return $this->className();
if($key === 'shortName') {
return str_replace('Fieldtype', '', $this->className());
} else if($key == 'longName' && method_exists($this, 'getModuleInfo')) {
$info = $this->getModuleInfo($this);
return $info['title'];
} else if($key === 'longName') {
$title = $this->wire()->modules->getModuleInfoProperty($this, 'title');
if(empty($title)) $title = $this->get('shortName');
return $title;
}
return parent::get($key);
}
@@ -1562,7 +1616,6 @@ abstract class Fieldtype extends WireData implements Module {
*
*/
public function ___install() {
if(false) throw new WireException(); // an offering for phpstorm
return true;
}
@@ -1582,13 +1635,19 @@ abstract class Fieldtype extends WireData implements Module {
public function ___uninstall() {
$names = array();
$fields = $this->wire('fields');
$fields = $this->wire()->fields;
foreach($fields as $field) {
/** @var Field $field */
if($field->type === $this->name) $names[] = $field->name;
}
if(count($names)) throw new WireException("Unable to uninstall Fieldtype '{$this->name}' because it is used by Fields: " . implode(", ", $names));
if(count($names)) {
throw new WireException(
"Unable to uninstall Fieldtype '$this->name' because it is used by Fields: " .
implode(", ", $names)
);
}
return true;
}
@@ -1605,7 +1664,6 @@ abstract class Fieldtype extends WireData implements Module {
*/
public function ___upgrade($fromVersion, $toVersion) {
// any code needed to upgrade between versions
if($fromVersion && $toVersion && false) throw new WireException(); // to make the ide stop complaining
}
/**

View File

@@ -5,7 +5,7 @@
*
* Interface and some functionality for Fieldtypes that can contain multiple values.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
* @method bool savePageFieldRows(Page $page, Field $field, $value)
@@ -79,7 +79,7 @@ abstract class FieldtypeMulti extends Fieldtype {
'label' => $this->_('count'),
'operators' => array('=', '!=', '<', '>', '<=', '>='),
'input' => 'number'
);
);
return $info;
}
@@ -92,7 +92,7 @@ abstract class FieldtypeMulti extends Fieldtype {
*/
public function ___getCompatibleFieldtypes(Field $field) {
$fieldtypes = $this->wire(new Fieldtypes());
foreach($this->wire('fieldtypes') as $fieldtype) {
foreach($this->wire()->fieldtypes as $fieldtype) {
if($fieldtype instanceof FieldtypeMulti) $fieldtypes->add($fieldtype);
}
return $fieldtypes;
@@ -217,8 +217,9 @@ abstract class FieldtypeMulti extends Fieldtype {
if(!$page->id || !$field->id) return false;
$database = $this->wire('database'); /** @var WireDatabasePDO $database */
$config = $this->wire('config'); /** @var Config $config */
$database = $this->wire()->database;
$config = $this->wire()->config;
$useTransaction = $database->allowTransaction();
$values = $page->get($field->name);
@@ -376,8 +377,7 @@ abstract class FieldtypeMulti extends Fieldtype {
if(!$page->id || !$field->id) return null;
/** @var WireDatabasePDO $database */
$database = $this->wire('database');
$database = $this->wire()->database;
$schema = $this->getDatabaseSchema($field);
$table = $database->escapeTable($field->table);
$stmt = null;
@@ -551,7 +551,7 @@ abstract class FieldtypeMulti extends Fieldtype {
if($limit > 0) {
// paginate the rows that will be loaded
if(is_null($start)) {
$pageNum = $this->wire('input')->pageNum() - 1;
$pageNum = $this->wire()->input->pageNum() - 1;
$start = $pageNum * $limit;
}
$start = (int) $start;
@@ -579,8 +579,7 @@ abstract class FieldtypeMulti extends Fieldtype {
*
*/
protected function getLoadQueryWhere(Field $field, DatabaseQuerySelect $query, $col, $operator, $value) {
if($field) {}
$database = $this->wire('database');
$database = $this->wire()->database;
$table = $query->data('_table');
if(empty($table)) $table = $field->getTable();
$table = $database->escapeTable($table);
@@ -697,7 +696,7 @@ abstract class FieldtypeMulti extends Fieldtype {
$sqls = array('pages_id=:pages_id');
$id = isset($item[$primaryKey]) ? $item[$primaryKey] : 0;
foreach($keys as $n => $key) {
foreach($keys as $key) {
$key = $database->escapeCol($key);
if($key === $primaryKey) continue;
$sqls[] = "`$key`=:$key";
@@ -805,8 +804,7 @@ abstract class FieldtypeMulti extends Fieldtype {
*
*/
protected function getMaxColumnValue(Page $page, Field $field, $column, $noValue = false) {
/** @var WireDatabasePDO $database */
$database = $this->wire('database');
$database = $this->wire()->database;
$table = $database->escapeTable($field->getTable());
$column = $database->escapeCol($column);
$sql = "SELECT MAX($column) FROM `$table` WHERE pages_id=:pages_id";
@@ -833,12 +831,16 @@ abstract class FieldtypeMulti extends Fieldtype {
*
*/
public function ___deletePageFieldRows(Page $page, Field $field, $value) {
$database = $this->wire()->database;
$info = $this->getDatabaseSchemaVerbose($field);
$primaryKeys = $info['primaryKeys'];
if(count($primaryKeys) !== 1) throw new WireException("deletePageFieldRows() can only be used on fields with 1 primary key");
if(count($primaryKeys) !== 1) {
throw new WireException("deletePageFieldRows() can only be used on fields with 1 primary key");
}
$value = $this->setupPageFieldRows($page, $field, $value);
$database = $this->wire('database');
$table = $database->escapeTable($info['table']);
$primaryKey = $database->escapeCol(reset($primaryKeys));
$ids = array();
@@ -864,17 +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;
$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;
}
@@ -898,7 +907,7 @@ abstract class FieldtypeMulti extends Fieldtype {
$n = self::$getMatchQueryCount;
$field = $query->field;
$database = $this->wire('database');
$database = $this->wire()->database;
$table = $database->escapeTable($table);
if($subfield === 'count'
@@ -916,7 +925,8 @@ abstract class FieldtypeMulti extends Fieldtype {
"SELECT $c.pages_id, COUNT($c.pages_id) AS num_$t " .
"FROM " . $database->escapeTable($field->table) . " AS $c " .
"GROUP BY $c.pages_id " .
") $t ON $t.pages_id=pages.id");
") $t ON $t.pages_id=pages.id"
);
if( (in_array($operator, array('<', '<=', '!=')) && $value) ||
(in_array($operator, array('>', '>=')) && $value < 0) ||
@@ -935,6 +945,7 @@ abstract class FieldtypeMulti extends Fieldtype {
if(count($templates)) {
$ids = array();
foreach($templates as $template) {
/** @var Template $template */
$ids[] = (int) $template->id;
}
$sql = 'pages.templates_id IN(' . implode(',', $ids) . ')'; // QA
@@ -983,8 +994,10 @@ abstract class FieldtypeMulti extends Fieldtype {
}
if(!empty($schema)) {
$fieldset = $this->wire('modules')->get('InputfieldFieldset');
$modules = $this->wire()->modules;
/** @var InputfieldFieldset $fieldset */
$fieldset = $modules->get('InputfieldFieldset');
$fieldset->attr('name', '_FieldtypeMultiExtras');
$fieldset->label = $this->_('Sorting and Pagination');
$fieldset->description = $this->_('These settings apply to both front-end (site) and back-end (editor).');
@@ -1000,7 +1013,8 @@ abstract class FieldtypeMulti extends Fieldtype {
}
$sorts = array_merge($sorts, $sortsReverse);
$f = $this->wire('modules')->get('InputfieldAsmSelect');
/** @var InputfieldAsmSelect $f */
$f = $modules->get('InputfieldAsmSelect');
$f->attr('name', 'orderByCols');
$f->label = $this->_('Automatic sorting');
$f->description = $this->_('Select one or more fields to sort by below. For manual sort, leave this setting blank.');
@@ -1012,7 +1026,8 @@ abstract class FieldtypeMulti extends Fieldtype {
// pagination support
if($this->get('usePagination') && count($primaryKeys) === 1) {
$f = $this->wire('modules')->get('InputfieldInteger');
/** @var InputfieldInteger $f */
$f = $modules->get('InputfieldInteger');
$f->attr('name', 'paginationLimit');
$f->label = $this->_('Pagination limit / items per page');
$f->description = $this->_('This limits the number of items loaded/edited per pagination. The value “0” indicates no limit (default).');
@@ -1030,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

@@ -1,7 +1,10 @@
<?php namespace ProcessWire;
/**
* Class FileCompiler
* FileCompiler
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @todo determine whether we should make storage in dedicated table rather than using wire('cache').
* @todo handle race conditions for multiple requests attempting to compile the same file(s).
@@ -127,6 +130,7 @@ class FileCompiler extends Wire {
if(strpos($sourcePath, '..') !== false) $sourcePath = realpath($sourcePath);
if(DIRECTORY_SEPARATOR != '/') $sourcePath = str_replace(DIRECTORY_SEPARATOR, '/', $sourcePath);
$this->sourcePath = rtrim($sourcePath, '/') . '/';
parent::__construct();
}
/**
@@ -135,8 +139,7 @@ class FileCompiler extends Wire {
*/
public function wired() {
/** @var Config $config */
$config = $this->wire('config');
$config = $this->wire()->config;
$globalOptions = $config->fileCompilerOptions;
if(is_array($globalOptions)) {
@@ -172,10 +175,10 @@ class FileCompiler extends Wire {
if(!$this->isWired()) $this->wired();
static $preloaded = false;
$config = $this->wire('config');
$config = $this->wire()->config;
if(!$preloaded) {
$this->wire('cache')->preloadFor($this);
$this->wire()->cache->preloadFor($this);
$preloaded = true;
}
@@ -184,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;
@@ -211,7 +215,7 @@ class FileCompiler extends Wire {
protected function mkdir($path, $recursive = true) {
$chmod = $this->globalOptions['chmodDir'];
if(empty($chmod) || !is_string($chmod) || strlen($chmod) < 2) $chmod = null;
return $this->wire('files')->mkdir($path, $recursive, $chmod);
return $this->wire()->files->mkdir($path, $recursive, $chmod);
}
/**
@@ -224,7 +228,7 @@ class FileCompiler extends Wire {
protected function chmod($filename) {
$chmod = $this->globalOptions['chmodFile'];
if(empty($chmod) || !is_string($chmod) || strlen($chmod) < 2) $chmod = null;
return $this->wire('files')->chmod($filename, false, $chmod);
return $this->wire()->files->chmod($filename, false, $chmod);
}
/**
@@ -256,7 +260,7 @@ class FileCompiler extends Wire {
$phpClose = '?' . '>';
$phpBlocks = explode($phpOpen, $data);
foreach($phpBlocks as $key => $phpBlock) {
foreach($phpBlocks as $phpBlock) {
$pos = strpos($phpBlock, $phpClose);
if($pos !== false) {
$closeBlock = substr($phpBlock, strlen($phpClose) + 2);
@@ -284,7 +288,6 @@ class FileCompiler extends Wire {
// remove single quoted blocks
$this->rawDequotedPHP = preg_replace('/([\s(.=,])\'[^\']*\'/s', '$1\'string\'', $this->rawDequotedPHP);
}
/**
@@ -299,7 +302,7 @@ class FileCompiler extends Wire {
if($this->globalOptions['siteOnly']) {
// only files in /site/ are allowed for compilation
if(strpos($filename, $this->wire('config')->paths->site) !== 0) {
if(strpos($filename, $this->wire()->config->paths->site) !== 0) {
// sourcePath is somewhere outside of the PW /site/, and not allowed
return false;
}
@@ -369,14 +372,14 @@ class FileCompiler extends Wire {
// target file already exists, check if it is up-to-date
// $targetData = file_get_contents($targetPathname);
$targetHash = md5_file($targetPathname);
$cache = $this->wire('cache')->getFor($this, $cacheName);
$cache = $this->wire()->cache->getFor($this, $cacheName);
if($cache && is_array($cache)) {
if($cache['target']['hash'] == $targetHash && $cache['source']['hash'] == $sourceHash) {
// target file is up-to-date
$compileNow = false;
} else {
// target file changed somewhere else, needs to be re-compiled
$this->wire('cache')->deleteFor($this, $cacheName);
$this->wire()->cache->deleteFor($this, $cacheName);
}
if(!$compileNow && isset($cache['source']['ns'])) {
$this->ns = $cache['source']['ns'];
@@ -389,7 +392,7 @@ class FileCompiler extends Wire {
$targetPath = dirname($targetPathname);
$targetData = file_get_contents($sourcePathname);
if(stripos($targetData, 'FileCompiler=0')) return $sourcePathname; // bypass if it contains this string
if(strpos($targetData, 'namespace') !== false) $this->ns = $this->wire('files')->getNamespace($targetData, true);
if(strpos($targetData, 'namespace') !== false) $this->ns = $this->wire()->files->getNamespace($targetData, true);
if(!$this->ns) $this->ns = "\\";
if(!__NAMESPACE__ && !$this->options['modules'] && $this->ns === "\\") return $sourcePathname;
set_time_limit(120);
@@ -416,7 +419,7 @@ class FileCompiler extends Wire {
'time' => filemtime($targetPathname),
)
);
$this->wire('cache')->saveFor($this, $cacheName, $cacheData, WireCache::expireNever);
$this->wire()->cache->saveFor($this, $cacheName, $cacheData, WireCache::expireNever);
}
}
@@ -427,7 +430,7 @@ class FileCompiler extends Wire {
// show notices about compiled files, when applicable
if($compileNow) {
$message = $this->_('Compiled file:') . ' ' . str_replace($this->wire('config')->paths->root, '/', $sourcePathname);
$message = $this->_('Compiled file:') . ' ' . str_replace($this->wire()->config->paths->root, '/', $sourcePathname);
if($this->globalOptions['showNotices']) {
$u = $this->wire('user');
if($u && $u->isSuperuser()) $this->message($message);
@@ -484,7 +487,7 @@ class FileCompiler extends Wire {
if($this->options['modules']) {
// FileCompiler modules
$compilers = array();
foreach($this->wire('modules')->findByPrefix('FileCompiler', true) as $module) {
foreach($this->wire()->modules->findByPrefix('FileCompiler', true) as $module) {
if(!$module instanceof FileCompilerModule) continue;
$runOrder = (int) $module->get('runOrder');
while(isset($compilers[$runOrder])) $runOrder++;
@@ -694,7 +697,7 @@ class FileCompiler extends Wire {
}
// replace absolute root path references with runtime generated versions
$rootPath = $this->wire('config')->paths->root;
$rootPath = $this->wire()->config->paths->root;
if(strpos($data, $rootPath)) {
$ns = __NAMESPACE__ ? "\\ProcessWire" : "";
$data = preg_replace('%([\'"])' . preg_quote($rootPath) . '([^\'"\s\r\n]*[\'"])%',
@@ -823,7 +826,7 @@ class FileCompiler extends Wire {
static $files = null;
if(is_null($files)) {
$files = array();
foreach(new \DirectoryIterator($this->wire('config')->paths->core) as $file) {
foreach(new \DirectoryIterator($this->wire()->config->paths->core) as $file) {
if($file->isDot() || $file->isDir()) continue;
$basename = $file->getBasename('.php');
if(strtoupper($basename[0]) == $basename[0]) {
@@ -834,12 +837,12 @@ class FileCompiler extends Wire {
}
// also add in all modules
foreach($this->wire('modules') as $module) {
foreach($this->wire()->modules as $module) {
$name = __NAMESPACE__ ? $module->className(true) : $module->className();
if(!in_array($name, $classes)) $classes[] = $name;
}
$classes = array_merge($classes, $files);
if(!__NAMESPACE__) $classes = array_merge($classes, array_keys($this->wire('modules')->getInstallable()));
if(!__NAMESPACE__) $classes = array_merge($classes, array_keys($this->wire()->modules->getInstallable()));
$rawPHP = $this->rawPHP;
$rawDequotedPHP = $this->rawDequotedPHP;
@@ -848,7 +851,6 @@ class FileCompiler extends Wire {
foreach($classes as $class) {
if(__NAMESPACE__ && strpos($class, __NAMESPACE__ . '\\') !== 0) continue; // limit only to ProcessWire classes/interfaces
/** @noinspection PhpUnusedLocalVariableInspection */
if(strpos($class, '\\') !== false) {
list($ns, $class) = explode('\\', $class, 2); // reduce to just class without namespace
} else {
@@ -903,7 +905,6 @@ class FileCompiler extends Wire {
$ns = '';
}
if($ns) {}
/** @noinspection PhpUnusedLocalVariableInspection */
if(stripos($rawDequotedPHP, $function) === false) continue; // if function name not mentioned in data, quick exit
$n = 0;
@@ -953,11 +954,12 @@ class FileCompiler extends Wire {
// don't perform full copies of some directories
// @todo convert this to use the user definable exclusions list
if($source === $this->wire('config')->paths->site) return 0;
if($source === $this->wire('config')->paths->siteModules) return 0;
if($source === $this->wire('config')->paths->templates) return 0;
$config = $this->wire()->config;
if($source === $config->paths->site) return 0;
if($source === $config->paths->siteModules) return 0;
if($source === $config->paths->templates) return 0;
if(!is_dir($target)) $this->wire('files')->mkdir($target, true);
if(!is_dir($target)) $this->wire()->files->mkdir($target, true);
$dir = new \DirectoryIterator($source);
$numCopied = 0;
@@ -993,7 +995,7 @@ class FileCompiler extends Wire {
}
if(!$numCopied) {
$this->wire('files')->rmdir($target, true);
$this->wire()->files->rmdir($target, true);
}
return $numCopied;
@@ -1044,13 +1046,13 @@ class FileCompiler extends Wire {
public function clearCache($all = false) {
if($all) {
$targetPath = $this->cachePath;
$this->wire('cache')->deleteFor($this);
$this->wire()->cache->deleteFor($this);
} else {
$this->init();
$targetPath = $this->targetPath;
}
if(!is_dir($targetPath)) return true;
return $this->wire('files')->rmdir($targetPath, true);
return $this->wire()->files->rmdir($targetPath, true);
}
/**
@@ -1089,11 +1091,14 @@ class FileCompiler extends Wire {
*
*/
protected function _maintenance($sourcePath, $targetPath) {
$config = $this->wire()->config;
$files = $this->wire()->files;
$sourcePath = rtrim($sourcePath, '/') . '/';
$targetPath = rtrim($targetPath, '/') . '/';
$sourceURL = str_replace($this->wire('config')->paths->root, '/', $sourcePath);
$targetURL = str_replace($this->wire('config')->paths->root, '/', $targetPath);
$sourceURL = str_replace($config->paths->root, '/', $sourcePath);
$targetURL = str_replace($config->paths->root, '/', $targetPath);
$useLog = $this->globalOptions['logNotices'];
//$this->log("Running maintenance for $targetURL (source: $sourceURL)");
@@ -1111,7 +1116,7 @@ class FileCompiler extends Wire {
if($file->isDir()) {
if(!is_dir($sourceFile)) {
$this->wire('files')->rmdir($targetFile, true);
$files->rmdir($targetFile, true);
if($useLog) $this->log("Maintenance/Remove directory: $targetURL$basename");
} else {
$this->_maintenance($sourceFile, $targetFile);
@@ -1121,7 +1126,7 @@ class FileCompiler extends Wire {
if(!file_exists($sourceFile)) {
// source file has been deleted
$this->wire('files')->unlink($targetFile, true);
$files->unlink($targetFile, true);
if($useLog) $this->log("Maintenance/Remove target file: $targetURL$basename");
} else if(filemtime($sourceFile) > filemtime($targetFile)) {
@@ -1198,4 +1203,3 @@ class FileCompiler extends Wire {
}
}

View File

@@ -43,6 +43,7 @@ abstract class FileCompilerModule extends WireData implements Module, Configurab
public function __construct() {
$this->set('runOrder', 0);
parent::__construct();
}
/**
@@ -60,7 +61,7 @@ abstract class FileCompilerModule extends WireData implements Module, Configurab
* 2. If you only want to compile non-PHP sections of the file, implement the compileMarkup() method instead.
*
* @param string $data
* @return string
* @return string|array
*
*/
public function compile($data) {
@@ -89,7 +90,7 @@ abstract class FileCompilerModule extends WireData implements Module, Configurab
if(!preg_match_all('!\?>(.+?)<\?!s', $data, $matches)) return array();
foreach($matches[1] as $key => $markup) {
foreach($matches[1] as $markup) {
$_markup = $this->compileMarkup($markup);
if($_markup !== $markup) {
$data = str_replace("?>$markup<?", "?>$_markup<?", $data);
@@ -154,7 +155,7 @@ abstract class FileCompilerModule extends WireData implements Module, Configurab
*
*/
public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
$f = $this->wire('modules')->get('InputfieldInteger');
$f = $inputfields->InputfieldInteger;
$f->attr('name', 'runOrder');
$f->attr('value', (int) $this->get('runOrder'));
$f->label = $this->_('Runtime execution order');

View File

@@ -5,7 +5,7 @@
*
* Creates and maintains a text-based log file.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
@@ -89,6 +89,7 @@ class FileLog extends Wire {
*
*/
public function __construct($path, $identifier = '') {
parent::__construct();
if($identifier) {
$path = rtrim($path, '/') . '/';
@@ -98,19 +99,32 @@ class FileLog extends Wire {
$path = dirname($path) . '/';
}
$this->path = $path;
if(!is_dir($path)) $this->wire('files')->mkdir($path);
}
public function __get($key) {
if($key == 'delimiter') return $this->delimeter; // @todo learn how to spell
return parent::__get($key);
/**
* Wired to API
*
*/
public function wired() {
parent::wired();
$this->path();
}
/**
* @param string $name
* @return mixed
*
*/
public function __get($name) {
if($name == 'delimiter') return $this->delimeter; // @todo learn how to spell
return parent::__get($name);
}
/**
* Clean a string for use in a log file entry
*
* @param $str
* @return mixed|string
* @return string
*
*/
protected function cleanStr($str) {
@@ -219,7 +233,7 @@ class FileLog extends Wire {
// if we were creating the file, make sure it has the right permission
if($mode === 'w') {
$files = $this->wire('files'); /** @var WireFileTools $files */
$files = $this->wire()->files;
$files->chmod($this->logFilename);
}
@@ -271,18 +285,42 @@ class FileLog extends Wire {
}
}
/**
* Get filesize
*
* @return int|false
*
*/
public function size() {
return filesize($this->logFilename);
}
/**
* Get file basename
*
* @return string
*
*/
public function filename() {
return basename($this->logFilename);
}
/**
* Get file pathname
*
* @return string|bool
*
*/
public function pathname() {
return $this->logFilename;
}
/**
* Get file modification time
*
* @return int|false
*
*/
public function mtime() {
return filemtime($this->logFilename);
}
@@ -483,7 +521,7 @@ class FileLog extends Wire {
}
if($options['toFile']) {
$toFile = $this->path . basename($options['toFile']);
$toFile = $this->path() . basename($options['toFile']);
$fp = fopen($toFile, 'w');
if(!$fp) throw new \Exception("Unable to open file for writing: $toFile");
} else {
@@ -536,7 +574,7 @@ class FileLog extends Wire {
if($fp) {
fclose($fp);
$this->wire('files')->chmod($toFile);
$this->wire()->files->chmod($toFile);
return $cnt;
}
@@ -553,8 +591,7 @@ class FileLog extends Wire {
* @param $line
* @param array $options
* @param bool $stopNow Populates this with true when it can determine no more lines are necessary.
* @return bool|int Returns boolean true if valid, false if not.
* If valid as a result of a date comparison, the unix timestmap for the line is returned.
* @return bool Returns boolean true if valid, false if not.
*
*/
protected function isValidLine($line, array $options, &$stopNow) {
@@ -623,13 +660,15 @@ class FileLog extends Wire {
fclose($fpw);
fclose($fpr);
$files = $this->wire()->files;
if($cnt) {
$this->wire('files')->unlink($filename, true);
$this->wire('files')->rename("$filename.new", $filename, true);
$this->wire('files')->chmod($filename);
$files->unlink($filename, true);
$files->rename("$filename.new", $filename, true);
$files->chmod($filename);
} else {
$this->wire('files')->unlink("$filename.new", true);
$files->unlink("$filename.new", true);
}
return $cnt;
@@ -651,8 +690,9 @@ class FileLog extends Wire {
'dateTo' => time(),
));
if(file_exists($toFile)) {
$this->wire('files')->unlink($this->logFilename, true);
$this->wire('files')->rename($toFile, $this->logFilename, true);
$files = $this->wire()->files;
$files->unlink($this->logFilename, true);
$files->rename($toFile, $this->logFilename, true);
return $qty;
}
return 0;
@@ -665,7 +705,7 @@ class FileLog extends Wire {
*
*/
public function delete() {
return $this->wire('files')->unlink($this->logFilename, true);
return $this->wire()->files->unlink($this->logFilename, true);
}
public function __toString() {
@@ -704,6 +744,16 @@ class FileLog extends Wire {
if($chunkSize > 0) $this->chunkSize = (int) $chunkSize;
return $this->chunkSize;
}
/**
* Get path where the log is stored (with trailing slash)
* @return string
*
*/
public function path() {
if(!is_dir($this->path)) $this->wire()->files->mkdir($this->path);
return $this->path;
}
}

View File

@@ -5,12 +5,12 @@
*
* Manages array of filenames or file URLs, like for $config->scripts and $config->styles.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class FilenameArray implements \IteratorAggregate, \Countable {
class FilenameArray extends Wire implements \IteratorAggregate, \Countable {
/**
* Array of filenames indexed by MD5 hash of filename
@@ -41,6 +41,7 @@ class FilenameArray implements \IteratorAggregate, \Countable {
*
*/
protected function getKey($filename) {
$filename = (string) $filename;
$pos = strpos($filename, '?');
$key = $pos ? substr($filename, 0, $pos) : $filename;
return md5($key);
@@ -86,6 +87,23 @@ class FilenameArray implements \IteratorAggregate, \Countable {
return new \ArrayObject($this->data);
}
/**
* Get cache-busting URLs for this FilenameArray
*
* This is the same as iterating this FilenameArray except that it appends cache-busting
* query strings to the URLs that resolve to physical files.
*
* @param bool|null|string $useVersion See Config::versionUrls() for arument details
* @return array
* @throws WireException
* @see Config::versionUrls()
* @since 3.0.227
*
*/
public function urls($useVersion = null) {
return $this->wire()->config->versionUrls($this, $useVersion);
}
/**
* Make FilenameArray unique (deprecated)
*
@@ -123,6 +141,30 @@ class FilenameArray implements \IteratorAggregate, \Countable {
return $this;
}
/**
* Replace one file with another
*
* @param string $oldFile
* @param string $newFile
* @return $this
* @since 3.0.215
*
*/
public function replace($oldFile, $newFile) {
$key = $this->getKey($oldFile);
if(isset($this->data[$key])) {
$this->data[$key] = $newFile;
} else {
$key = array_search($oldFile, $this->data);
if($key !== false) {
$this->data[$key] = $newFile;
} else {
$this->add($newFile);
}
}
return $this;
}
/**
* String value containing print_r() dump of all filenames
*

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";
}
/**
@@ -680,7 +731,7 @@ function wireIconMarkupFile($filename, $class = '') {
}
/**
* Given a quantity of bytes (int), return readable string that refers to quantity in bytes, kB, MB, GB, etc.
* Given a quantity of bytes (int), return readable string that refers to quantity in bytes, kB, MB, GB and TB
*
* #pw-group-strings
*
@@ -690,96 +741,31 @@ function wireIconMarkupFile($filename, $class = '') {
* - `1` (int): Same as `true` but with space between number and unit label.
* - Or optionally specify the $options argument here if you do not need the $small argument.
* @param array|int $options Options to modify default behavior, or if an integer then `decimals` option is assumed:
* - `decimals` (int): Number of decimals to use in returned value (default=0).
* - `decimals` (int|null): Number of decimals to use in returned value or NULL for auto (default=null).
* When null (auto) a decimal value of 1 is used when appropriate, for megabytes and higher (3.0.214+).
* - `decimal_point` (string|null): Decimal point character, or null to detect from locale (default=null).
* - `thousands_sep` (string|null): Thousands separator, or null to detect from locale (default=null).
* - `small` (bool): If no $small argument was specified, you can optionally specify it in this $options array.
* - `type` (string): To force return value as specific type, specify one of: bytes, kilobytes, megabytes, gigabytes; or just: b, k, m, g. (3.0.148+ only)
* - `type` (string): To force return value as specific type, specify one of: bytes, kilobytes, megabytes,
* gigabytes, terabytes; or just: b, k, m, g, t. (3.0.148+ only, terabytes 3.0.214+).
* @return string
*
*/
function wireBytesStr($bytes, $small = false, $options = array()) {
$defaults = array(
'type' => '',
'decimals' => 0,
'decimal_point' => null,
'thousands_sep' => null,
);
if(is_array($small)) {
$options = $small;
$small = isset($options['small']) ? $options['small'] : false;
}
if(!is_array($options)) $options = array('decimals' => (int) $options);
if(!is_int($bytes)) $bytes = (int) $bytes;
$options = array_merge($defaults, $options);
$locale = array();
$type = empty($options['type']) ? '' : strtolower(substr($options['type'], 0, 1));
// determine size value and units label
if($bytes < 1024 || $type === 'b') {
$val = $bytes;
if($small) {
$label = $val > 0 ? __('B', __FILE__) : ''; // bytes
} else if($val == 1) {
$label = __('byte', __FILE__); // singular 1-byte
if(is_array($small)) $options = $small;
if(!is_array($options)) {
if(ctype_digit("$options")) {
$options = array('decimals' => (int) $options);
} else {
$label = __('bytes', __FILE__); // plural 2+ bytes (or 0 bytes)
}
} else if($bytes < 1000000 || $type === 'k') {
$val = $bytes / 1024;
$label = __('kB', __FILE__); // kilobytes
} else if($bytes < 1073741824 || $type === 'm') {
$val = $bytes / 1024 / 1024;
$label = __('MB', __FILE__); // megabytes
} else {
$val = $bytes / 1024 / 1024 / 1024;
$label = __('GB', __FILE__); // gigabytes
}
// determine decimal point if not specified in $options
if($options['decimal_point'] === null) {
if($options['decimals'] > 0) {
// determine decimal point from locale
if(empty($locale)) $locale = localeconv();
$options['decimal_point'] = empty($locale['decimal_point']) ? '.' : $locale['decimal_point'];
} else {
// no decimal point needed (not used)
$options['decimal_point'] = '.';
$options = array();
}
}
// determine thousands separator if not specified in $options
if($options['thousands_sep'] === null) {
if($small || $val < 1000) {
// no thousands separator needed
$options['thousands_sep'] = '';
} else {
// get thousands separator from current locale
if(empty($locale)) $locale = localeconv();
$options['thousands_sep'] = empty($locale['thousands_sep']) ? '' : $locale['thousands_sep'];
}
if(is_int($small) && !isset($options['decimals'])) {
$options['decimals'] = $small;
} else if(is_bool($small)) {
$options['small'] = $small;
}
// format number to string
$str = number_format($val, $options['decimals'], $options['decimal_point'], $options['thousands_sep']);
// in small mode remove numbers with decimals that consist only of zeros "0"
if($small && $options['decimals'] > 0) {
$test = substr($str, -1 * $options['decimals']);
if(((int) $test) === 0) {
$str = substr($str, 0, strlen($str) - ($options['decimals'] + 1)); // i.e. 123.00 => 123
} else {
$str = rtrim($str, '0'); // i.e. 123.10 => 123.1
}
}
// append units label to number
$str .= ($small === true ? '' : ' ') . $label;
return $str;
return wire()->sanitizer->getNumberTools()->bytesToStr($bytes, $options);
}
/**
@@ -986,14 +972,17 @@ function wireClassExists($className, $autoload = true) {
*
* #pw-group-class-helpers
*
* @param string $className
* @param string $method
* @param string $className Class name or object
* @param string $method Method name
* @param bool $hookable Also return true if "method" exists in a hookable format "___method"? (default=false) 3.0.204+
* @return bool
*
*/
function wireMethodExists($className, $method) {
function wireMethodExists($className, $method, $hookable = false) {
if(!is_object($className)) $className = wireClassName($className, true);
return method_exists($className, $method);
$exists = method_exists($className, $method);
if(!$exists && $hookable) $exists = method_exists($className, "___$method");
return $exists;
}
/**
@@ -1155,7 +1144,7 @@ function wireInstanceOf($instance, $className, $autoload = true) {
* @param string|callable $var
* @param bool $syntaxOnly
* @var string $callableName
* @return array
* @return bool
*
*/
function wireIsCallable($var, $syntaxOnly = false, &$callableName = '') {
@@ -1181,7 +1170,7 @@ function wireIsCallable($var, $syntaxOnly = false, &$callableName = '') {
function wireCount($value) {
if($value === null) return 0;
if(is_array($value)) return count($value);
if(is_object($value) && $value instanceof \Countable) return count($value);
if($value instanceof \Countable) return count($value);
return 1;
}
@@ -1454,5 +1443,3 @@ function PageArray($items = array()) {
$pa = PageArray::newInstance($items);
return $pa;
}

View File

@@ -100,6 +100,23 @@ function page($key = '', $value = null) {
return wirePage($key, $value);
}
/**
* Return id for given page or false if its not a page
*
* Returns positive int (page id) for page that exists, 0 for NullPage,
* or false if given $value is not a Page.
*
* #pw-group-Functions-API
*
* @param Page|mixed $value
* @return int|false
* @since 3.0.224
*
*/
function pageId($value) {
return wirePageId($value);
}
/**
* Access a ProcessWire configuration setting ($config API variable as a function)
*
@@ -680,4 +697,3 @@ function region($key = '', $value = null) {
function setting($name = '', $value = null) {
return wireSetting($name, $value);
}

View File

@@ -132,6 +132,21 @@ function wirePage($key = '', $value = null) {
return _wireDataAPI('page', $key, $value);
}
/**
* Return id for given page or false if its not a page
*
* Returns positive int (page id) for page that exists, 0 for NullPage,
* or false if given $value is not a Page.
*
* @param Page|mixed $value
* @return int|false
* @since 3.0.224
*
*/
function wirePageId($value) {
return ($value instanceof Page ? $value->id : false);
}
/**
* Access the $config API variable as a function
*
@@ -166,9 +181,11 @@ function wireConfig($key = '', $value = null) {
*
*/
function wireModules($name = '') {
/** @var Modules $modules */
$modules = wire('modules');
return strlen($name) ? $modules->getModule($name) : $modules;
$name = (string) $name;
$modules = wire()->modules;
/** @var Modules|Module|ConfigurableModule|null $value */
$value = strlen($name) ? $modules->getModule($name) : $modules;
return $value;
}
/**
@@ -189,12 +206,14 @@ function wireUser($key = '', $value = null) {
* See the pages() function for full usage details.
*
* @param string|array $selector Optional selector to send to find() or get()
* @return Users|PageArray|User|mixed
* @return Users|PageArray|User|NullPage|mixed
* @see pages()
*
*/
function wireUsers($selector = '') {
return _wirePagesAPI('users', $selector);
/** @var Users|PageArray|User|NullPage|mixed $value */
$value = _wirePagesAPI('users', $selector);
return $value;
}
/**
@@ -217,8 +236,7 @@ function wireSession($key = '', $value = null) {
*
*/
function wireFields($name = '') {
/** @var Fields $fields */
$fields = wire('fields');
$fields = wire()->fields;
return strlen($name) ? $fields->get($name) : $fields;
}
@@ -230,8 +248,7 @@ function wireFields($name = '') {
*
*/
function wireTemplates($name = '') {
/** @var Templates $templates */
$templates = wire('templates');
$templates = wire()->templates;
return strlen($name) ? $templates->get($name) : $templates;
}
@@ -242,7 +259,7 @@ function wireTemplates($name = '') {
*
*/
function wireDatabase() {
return wire('database');
return wire()->database;
}
/**
@@ -255,7 +272,9 @@ function wireDatabase() {
*
*/
function wirePermissions($selector = '') {
return _wirePagesAPI('permissions', $selector);
/** @var Permissions|Permission|PageArray|null|NullPage $value */
$value = _wirePagesAPI('permissions', $selector);
return $value;
}
/**
@@ -268,7 +287,9 @@ function wirePermissions($selector = '') {
*
*/
function wireRoles($selector = '') {
return _wirePagesAPI('roles', $selector);
/** @var Roles|Role|PageArray|null|NullPage $value */
$value = _wirePagesAPI('roles', $selector);
return $value;
}
/**
@@ -286,7 +307,8 @@ function wireRoles($selector = '') {
*
*/
function wireSanitizer($name = '', $value = '') {
$sanitizer = wire('sanitizer');
$name = (string) $name;
$sanitizer = wire()->sanitizer;
return strlen($name) ? $sanitizer->$name($value) : $sanitizer;
}
@@ -306,8 +328,7 @@ function wireSanitizer($name = '', $value = '') {
*
*/
function wireDatetime($format = '', $value = '') {
/** @var WireDateTime $datetime */
$datetime = wire('datetime');
$datetime = wire()->datetime;
return strlen($format) ? $datetime->formatDate($value ? $value : time(), $format) : $datetime;
}
@@ -318,7 +339,7 @@ function wireDatetime($format = '', $value = '') {
*
*/
function wireFiles() {
return wire('files');
return wire()->files;
}
/**
@@ -335,9 +356,7 @@ function wireFiles() {
*
*/
function wireCache($name = '', $expire = null, $func = null) {
/** @var WireCache $cache */
$cache = wire('cache');
return strlen($name) ? $cache->get($name, $expire, $func) : $cache;
return strlen($name) ? wire()->cache->get($name, $expire, $func) : wire()->cache;
}
/**
@@ -357,8 +376,7 @@ function wireCache($name = '', $expire = null, $func = null) {
*
*/
function wireLanguages($name = '') {
/** @var Languages $languages */
$languages = wire('languages');
$languages = wire()->languages;
if(!$languages) return null;
if(strlen($name)) return $languages->get($name);
return $languages;
@@ -392,8 +410,7 @@ function wireLanguages($name = '') {
*
*/
function wireInput($type = '', $key = '', $sanitizer = null, $fallback = null) {
/** @var WireInput $input */
$input = wire('input');
$input = wire()->input;
if(!strlen($type)) return $input;
$type = strtolower($type);
if(!strlen($key)) return $input->$type;
@@ -458,8 +475,7 @@ function wireInputCookie($key = '', $sanitizer = null, $fallback = null) {
*
*/
function wireLog($logName = '', $message = '') {
/** @var WireLog $log */
$log = wire('log');
$log = wire()->log;
if(strlen($message)) {
if(!strlen($logName)) $logName = 'unknown';
return $log->save($logName, $message);
@@ -477,13 +493,14 @@ function wireLog($logName = '', $message = '') {
*
*/
function wireProfiler($name = null, $source = null, $data = array()) {
$profiler = wire('profiler');
$profiler = wire()->profiler;
if(is_null($name)) return $profiler;
if(!$profiler) return null;
if(is_string($name)) {
return $profiler->start($name, $source, $data);
} else {
return $profiler->stop($name);
$profiler->stop($name);
return null;
}
}
@@ -495,8 +512,8 @@ function wireProfiler($name = null, $source = null, $data = array()) {
*
*/
function wireUrls($key = '') {
if(empty($key)) return wire('config')->urls;
return wire('config')->urls($key);
if(empty($key)) return wire()->config->urls;
return wire()->config->urls($key);
}
/**
@@ -507,8 +524,8 @@ function wireUrls($key = '') {
*
*/
function wirePaths($key = '') {
if(empty($key)) return wire('config')->paths;
return wire('config')->paths($key);
if(empty($key)) return wire()->config->paths;
return wire()->config->paths($key);
}
/**
@@ -594,4 +611,3 @@ function _wireFunctionsAPI() {
);
return $names;
}

View File

@@ -3,7 +3,7 @@
/**
* ProcessWire HookEvent
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* Instances of HookEvent are passed to Hook handlers when their requested method has been called.
@@ -60,6 +60,7 @@ class HookEvent extends WireData {
);
if(!empty($eventData)) $data = array_merge($data, $eventData);
$this->data = $data;
parent::__construct();
}
/**
@@ -130,7 +131,6 @@ class HookEvent extends WireData {
return array_key_exists($key, $arguments) ? $arguments[$key] : null;
}
$value = null;
$argumentsByName = array();
foreach($names as $key => $name) {
@@ -161,8 +161,9 @@ class HookEvent extends WireData {
if(is_string($n) && !ctype_digit($n)) {
// convert argument name to position
$names = $this->getArgumentNames();
$n = array_search($n, $names);
if($n === false) throw new WireException("Unknown argument name: $n");
$name = $n;
$n = array_search($name, $names);
if($n === false) throw new WireException("Unknown argument name: $name");
}
$this->data['arguments'][(int)$n] = $value;

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);
}
@@ -862,12 +862,13 @@ class ImageSizerEngineGD extends ImageSizerEngine {
} else if($this->imageType == IMAGETYPE_GIF) {
// @mrx GIF transparency
$transparentIndex = imagecolortransparent($image);
// $transparentColor = $transparentIndex != -1 ? @imagecolorsforindex($image, $transparentIndex) : 0;
$transparentColor = $transparentIndex != -1 ? imagecolorsforindex($image, ($transparentIndex < imagecolorstotal($image) ? $transparentIndex : $transparentIndex - 1)) : 0;
if(!empty($transparentColor)) {
$transparentNew = imagecolorallocate($im, $transparentColor['red'], $transparentColor['green'], $transparentColor['blue']);
$transparentNewIndex = imagecolortransparent($im, $transparentNew);
imagefill($im, 0, 0, $transparentNewIndex);
if($transparentIndex >= 0 && $transparentIndex < imagecolorstotal($image)) {
$transparentColor = imagecolorsforindex($image, $transparentIndex);
if(!empty($transparentColor)) {
$transparentNew = imagecolorallocate($im, $transparentColor['red'], $transparentColor['green'], $transparentColor['blue']);
$transparentNewIndex = imagecolortransparent($im, $transparentNew);
imagefill($im, 0, 0, $transparentNewIndex);
}
}
} else {

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
@@ -59,8 +59,10 @@
* @property string $icon Optional font-awesome icon name to accompany label (excluding the "fa-") part). #pw-group-labels
* @property string $requiredLabel Optional custom label to display when missing required value. @since 3.0.98 #pw-group-labels
* @property string $head Optional text that appears below label but above description (only used by some Inputfields). #pw-internal
* @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
@@ -96,19 +98,21 @@
* ===================
* @property int|bool $required Set to true (or 1) to make input required, or false (or 0) to make not required (default=0). #pw-group-behavior
* @property string $requiredIf Optional conditions under which input is required (selector string). #pw-group-behavior
* @property int|bool|null $requiredAttr Use HTML5 “required” attribute when used by Inputfield and $required is true? Default=null. #pw-group-behavior
* @property int|bool|null $requiredAttr Use HTML5 “required” attribute when used by Inputfield and $required is true? Default=null. #pw-group-behavior
* @property InputfieldWrapper|null $parent The parent InputfieldWrapper for this Inputfield or null if not set. #pw-internal
* @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). @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). @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
@@ -118,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()
@@ -218,6 +226,30 @@ abstract class Inputfield extends WireData implements Module {
*/
const collapsedBlankAjax = 11;
/**
* Collapsed into a separate tab
* #pw-group-collapsed-constants
* @since 3.0.201
*
*/
const collapsedTab = 20;
/**
* Collapsed into a separate tab and AJAX loaded
* #pw-group-collapsed-constants
* @since 3.0.201
*
*/
const collapsedTabAjax = 21;
/**
* Collapsed into a separate tab and locked (not editable)
* #pw-group-collapsed-constants
* @since 3.0.201
*
*/
const collapsedTabLocked = 22;
/**
* Don't skip the label (default)
* #pw-group-skipLabel-constants
@@ -274,6 +306,20 @@ abstract class Inputfield extends WireData implements Module {
*
*/
const textFormatMarkdown = 8;
/**
* Render flags: place first in render
* #pw-group-render-constants
*
*/
const renderFirst = 1;
/**
* Render flags: place last in render
* #pw-group-render-constants
*
*/
const renderLast = 2;
/**
* Render only the minimum output when in "renderValue" mode.
@@ -343,7 +389,7 @@ abstract class Inputfield extends WireData implements Module {
protected $defaultID = '';
/**
* Whether or not this Inputfield is editable
* Whether this Inputfield is editable
*
* When false, its processInput method won't be called by InputfieldWrapper's processInput
*
@@ -352,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
*
@@ -366,6 +420,7 @@ abstract class Inputfield extends WireData implements Module {
$this->set('notes', ''); // highlighted descriptive copy, below output of input field
$this->set('detail', ''); // text details that appear below notes
$this->set('head', ''); // below label, above description
$this->set('tabLabel', ''); // alternate label for tab when Inputfield::collapsedTab* in use
$this->set('required', 0); // set to 1 to make value required for this field
$this->set('requiredIf', ''); // optional conditions to make it required
$this->set('collapsed', ''); // see the collapsed* constants at top of class (use blank string for unset value)
@@ -375,10 +430,13 @@ abstract class Inputfield extends WireData implements Module {
$this->set('wrapClass', ''); // optional class to apply to the Inputfield wrapper (contains InputfieldHeader + InputfieldContent)
$this->set('headerClass', ''); // optional class to apply to InputfieldHeader wrapper
$this->set('contentClass', ''); // optional class to apply to InputfieldContent wrapper
$this->set('addClass', ''); // space-separated classes to add, optionally specifying element (see addClassString method)
$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;
@@ -389,6 +447,8 @@ abstract class Inputfield extends WireData implements Module {
$value = $this instanceof InputfieldHasArrayValue ? array() : null;
$this->setAttribute('value', $value);
parent::__construct();
}
/**
@@ -451,17 +511,32 @@ abstract class Inputfield extends WireData implements Module {
*
*/
public function set($key, $value) {
if($key == 'parent' && ($value instanceof InputfieldWrapper)) return $this->setParent($value);
if($key == 'collapsed') {
if($key === 'parent') {
if($value instanceof InputfieldWrapper) return $this->setParent($value);
} else if($key === 'collapsed') {
if($value === true) $value = self::collapsedYes;
$value = (int) $value;
}
if(array_key_exists($key, $this->attributes)) return $this->setAttribute($key, $value);
if($key == 'required' && $value && !is_object($value)) $this->addClass('required');
if($key == 'columnWidth') {
} else if(array_key_exists($key, $this->attributes)) {
return $this->setAttribute($key, $value);
} else if($key === 'required' && $value && !is_object($value)) {
$this->addClass('required');
} else if($key === 'columnWidth') {
$value = (int) $value;
if($value < 10 || $value > 99) $value = '';
} else if($key === 'addClass') {
if(is_string($value) && !ctype_alnum($value)) {
$test = str_replace(array(' ', ':', ',', '-', '+', '=', '!', '_', '.', '@', "\n"), '', $value);
if(!ctype_alnum($test)) $value = preg_replace('/[^-+_:=@!,. a-zA-Z0-9\n]/', '', $value);
}
$this->addClass($value);
}
return parent::set($key, $value);
}
@@ -470,7 +545,7 @@ abstract class Inputfield extends WireData implements Module {
*
* - This can also be accessed directly, i.e. `$value = $inputfield->property;`.
*
* - For getting attribute values, this will work but it is preferable to use the `Inputfield::attr()` method.
* - For getting attribute values, this will work, but it is preferable to use the `Inputfield::attr()` method.
*
* - For getting non-attribute values that have potential name conflicts with attributes (or just as a
* reliable alternative), use the `Inputfield::getSetting()` method instead, which excludes the possibility
@@ -649,7 +724,7 @@ abstract class Inputfield extends WireData implements Module {
/**
* Set an attribute
*
* - For most public API use, you might consider using the the shorter `Inputfield::attr()` method instead.
* - For most public API use, you might consider using the shorter `Inputfield::attr()` method instead.
*
* - When setting the `class` attribute it is preferable to use the `Inputfield::addClass()` method.
*
@@ -961,7 +1036,7 @@ abstract class Inputfield extends WireData implements Module {
/**
* Add a class or classes to this Inputfield (or a wrapping element)
*
* If given a class name that's already present, it won't be added again.
* If given a class name thats already present, it wont be added again.
*
* ~~~~~
* // Add class "foobar" to input element
@@ -972,14 +1047,39 @@ abstract class Inputfield extends WireData implements Module {
*
* // Add class "foobar" to .Inputfield wrapping element
* $inputfield->addClass('foobar', 'wrapClass');
*
* // Add classes while specifying Inputfield element (3.0.204+)
* $inputfield->addClass('wrap:card, header:card-header, content:card-body');
* ~~~~~
*
* **Formatted string option (3.0.204+):**
* Classes can be added by formatted string that dictates what Inputfield element they
* should be added to, in the format `element:classNames` like in this example below:
* ~~~~~
* wrap:card card-default
* header:card-header
* content:card-body
* input:form-input input-checkbox
* ~~~~~
* Each line represents a group containing an element name and one or more space-separated
* classes. Groups may be separated by newline (like above) or with a comma. The element
* name may be any one of the following:
*
* - `wrap`: The .Inputfield element that wraps the header and content
* - `header`: The .InputfieldHeader element, typically a `<label>`.
* - `content`: The .InputfieldContent element that wraps the input(s), typically a `<div>`.
* - `input`: The primary `<input>` element(s) that accept input for the Inputfield.
* - `class`: This is the same as the 'input' type, just an alias.
*
* Class names prefixed with a minus sign i.e. `-class` will be removed rather than added.
*
* #pw-group-attribute-methods
*
* @param string|array $class Specify one of the following:
* - Class name you want to add.
* - Multiple space-separated class names you want to add.
* - Array of class names you want to add (since 3.0.16).
* - Formatted string of classes as described in method description (since 3.0.204+).
* @param string $property Optionally specify the type of class you want to add:
* - Omit for the default (which is "class").
* - `class` (string): Add class to the input element (or whatever the Inputfield default is).
@@ -987,51 +1087,39 @@ abstract class Inputfield extends WireData implements Module {
* - `headerClass` (string): Add class to ".InputfieldHeader" label element.
* - `contentClass` (string): Add class to ".InputfieldContent" wrapping element.
* - Or some other named class attribute designated by a descending Inputfield.
* - You can optionally omit the `Class` suffix in 3.0.204+, i.e. `wrap` rather than `wrapClass`.
* @return $this
* @throws WireException when given invalid arguments
* @see Inputfield::hasClass(), Inputfield::removeClass()
*
*/
public function addClass($class, $property = 'class') {
// determine which type of class we are adding, and get existing value
if($property == 'class' || empty($property)) {
$value = $this->getAttribute('class');
} else if($property == 'contentClass' || $property == 'content') {
$value = $this->contentClass;
} else if($property == 'wrapClass' || $property == 'wrap') {
$value = $this->wrapClass;
} else if($property == 'headerClass' || $property == 'header') {
$value = $this->headerClass;
} else if(!is_string($property)) {
throw new WireException("addClass() property name must be a string");
} else {
// some other class property unknown by this base class
$value = $this->getSetting($property);
if(!is_string($value) && !is_null($value)) throw new WireException("Invalid class property for addClass()");
if(is_null($value)) $value = '';
}
// classes is array of current classes
$classes = explode(' ', $value);
$force = strpos($property, '=') === 0; // force set, skip processing by addClassString
if($force) $property = ltrim($property, '=');
if(is_string($class) && !ctype_alnum($class) && !$force) {
if(strpos($class, ':') || strpos($class, "\n") || strpos($class, ",")) {
return $this->addClassString($class, $property);
}
}
$property = $this->getClassProperty($property);
$classes = $this->getClassArray($property, true);
// addClasses is array of classes being added
$addClasses = is_array($class) ? $class : explode(' ', $class);
// add to $classes array
foreach($addClasses as $addClass) {
$addClass = trim($addClass);
if(!strlen($addClass)) continue;
$classes[] = $addClass;
if(strlen($addClass)) $classes[$addClass] = $addClass;
}
$classes = array_unique($classes);
// convert back to string
$value = trim(implode(' ', $classes));
// set back to Inputfield
if($property == 'class') {
if($property === 'class') {
$this->attributes['class'] = $value;
} else {
$this->set($property, $value);
@@ -1040,6 +1128,83 @@ abstract class Inputfield extends WireData implements Module {
return $this;
}
/**
* Add class(es) by formatted string that lets you specify where class should be added
*
* To use this in the public API use `addClass()` method or set the `addClass` property
* with a formatted string value as indicated here.
*
* Allows for setting via formatted string like:
* ~~~~~
* wrap:card card-default
* header:card-header
* content:card-body
* input:form-input input-checkbox
* ~~~~~
* Each line represents a group containing a element type, colon, and one or more space-
* separated classes. Groups may be separated by newline (like above) or with a comma.
* The element type may be any one of the following:
*
* - `wrap`: The .Inputfield element that wraps the header and content
* - `header`: The .InputfieldHeader element, typically a `<label>`.
* - `content`: The .InputfieldContent element that wraps the input(s), typically a `<div>`.
* - `input`: The primary `<input>` element(s) that accept input for the Inputfield.
* - `class`: This is the same as the 'input' type, just an alias.
* - `+foo`: Force adding your own new element type (i.e. “foo”) that is not indicated above.
*
* Class names prefixed with a minus sign i.e. `-class` will be removed rather than added.
*
* A string like `hello:world` where `hello` is not one of those element types listed above,
* and is not prefixed with a plus sign `+`, will be added as a literal class name with the
* colon in it (such as those used by Tailwind).
*
* @param string $class Formatted class string to parse class types and names from
* @param string $property Default/fallback element/property if not indicated in string
* @return self
* @since 3.0.204
*
*
*/
protected function addClassString($class, $property = 'class') {
if(ctype_alnum($class)) return $this->addClass($class, $property);
$typeNames = array('wrap', 'header', 'content', 'input', 'class');
$class = trim($class);
if(strpos($class, "\n")) $class = str_replace("\n", ",", $class);
$groups = strpos($class, ',') ? explode(',', $class) : array($class);
foreach($groups as $group) {
$type = $property;
$group = trim($group);
$classes = explode(' ', $group);
foreach($classes as $class) {
if(empty($class)) continue;
if(strpos($class, ':')) {
// setting new element type i.e. wrap:myclass or +foo:myclass
list($typeName, $className) = explode(':', $class, 2);
$typeName = trim($typeName);
if(in_array($typeName, $typeNames) || strpos($typeName, '+') === 0) {
// accepted as element/type for adding classes
$type = ltrim($typeName, '+');
$class = trim($className);
} else {
// literal class name with a colon in it such as "lg:bg-red-400'
}
}
if(strpos($class, '-') === 0) {
$this->removeClass(ltrim($class, '-'), $type);
} else {
$this->addClass($class, "=$type"); // "=type" prevents further processing
}
}
}
return $this;
}
/**
* Does this Inputfield have the given class name (or names)?
*
@@ -1092,13 +1257,51 @@ abstract class Inputfield extends WireData implements Module {
}
// checking single class
if($property == 'class') {
$value = explode(' ', $this->getAttribute('class'));
} else {
$value = explode(' ', $this->$property);
$classes = $this->getClassArray($property, true);
return isset($classes[$class]);
}
/**
* Get classes in array for given class property
*
* @param string $property One of 'wrap', 'header', 'content' or 'input' (or alias 'class')
* @param bool $assoc Return as associative array where both keys and values are class names? (default=false)
* @return array
* @since 3.0.204
*
*/
public function getClassArray($property = 'class', $assoc = false) {
$property = $this->getClassProperty($property);
$value = ($property === 'class' ? $this->attr('class') : $this->getSetting($property));
$value = trim("$value");
while(strpos($value, ' ') !== false) $value = str_replace(' ', ' ', $value);
$classes = strlen($value) ? explode(' ', $value) : array();
if($assoc) {
$a = array();
foreach($classes as $class) $a[$class] = $class;
$classes = $a;
}
return in_array($class, $value);
return $classes;
}
/**
* Get the internal property name for given class property
*
* This converts things like 'wrap' to 'wrapClass', 'header' to 'headerClass', etc.
*
* @param string $property
* @return string
* @since 3.0.204
*
*/
protected function getClassProperty($property) {
if($property === 'class' || $property === 'input' || empty($property)) {
$property = 'class';
} else if(strpos($property, 'Class') === false) {
if(in_array($property, array('wrap', 'header', 'content'))) $property .= 'Class';
}
return $property;
}
/**
@@ -1132,22 +1335,16 @@ abstract class Inputfield extends WireData implements Module {
*
*/
public function removeClass($class, $property = 'class') {
if($property == 'class') {
$classes = explode(' ', $this->getAttribute('class'));
} else {
$classes = explode(' ', $this->$property);
}
$property = $this->getClassProperty($property);
$classes = $this->getClassArray($property, true);
$removeClasses = is_array($class) ? $class : explode(' ', $class);
foreach($removeClasses as $removeClass) {
if(!strlen($removeClass)) continue;
$key = array_search($removeClass, $classes);
if($key !== false) unset($classes[$key]);
if(strlen($removeClass)) unset($classes[$removeClass]);
}
if($property == 'class') {
if($property === 'class') {
$this->attributes['class'] = implode(' ', $classes);
} else {
$this->set($property, implode(' ', $classes));
@@ -1173,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 = '';
@@ -1198,13 +1395,18 @@ abstract class Inputfield extends WireData implements Module {
// if an attribute has multiple values (like class), then bundle them into a string separated by spaces
$value = implode(' ', $value);
} else if(is_bool($value)) {
// boolean attribute uses only attribute name when true, or omit when false
if($value === true) $str .= "$attr ";
continue;
} else if(!strlen("$value") && strpos($attr, 'data-') !== 0) {
// skip over empty non-data attributes that are not arrays
// if(!$value = $this->attr($attr))) continue; // was in 3.0.132 and earlier
continue;
}
$str .= "$attr=\"" . htmlspecialchars($value, ENT_QUOTES, "UTF-8") . '" ';
$str .= "$attr=\"" . htmlspecialchars("$value", ENT_QUOTES, "UTF-8") . '" ';
}
return trim($str);
@@ -1237,10 +1439,10 @@ abstract class Inputfield extends WireData implements Module {
if(is_array($value)) {
if(!count($value)) return '';
$out = "<ul>";
foreach($value as $v) $out .= "<li>" . $this->wire('sanitizer')->entities($v) . "</li>";
foreach($value as $v) $out .= "<li>" . $this->wire()->sanitizer->entities($v) . "</li>";
$out .= "</ul>";
} else {
$out = $this->wire('sanitizer')->entities($value);
$out = $this->wire()->sanitizer->entities($value);
}
return $out;
}
@@ -1261,16 +1463,18 @@ 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) {
if($parent) {}
if($renderValueMode) {}
$result = $this->wire('modules')->loadModuleFileAssets($this) > 0;
if($this->wire('hooks')->isMethodHooked($this, 'renderReadyHook')) {
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);
}
return $result;
@@ -1281,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
@@ -1438,87 +1642,91 @@ abstract class Inputfield extends WireData implements Module {
public function ___getConfigInputfields() {
$conditionsText = $this->_('Conditions are expressed with a "field=value" selector containing fields and values to match. Multiple conditions should be separated by a comma.');
$conditionsNote = $this->_('Read more about [how to use this](http://processwire.com/api/selectors/inputfield-dependencies/).');
$conditionsNote = $this->_('Read more about [how to use this](https://processwire.com/api/selectors/inputfield-dependencies/).');
/** @var InputfieldWrapper $fields */
/** @var InputfieldWrapper $inputfields */
$inputfields = $this->wire(new InputfieldWrapper());
$fieldset = $inputfields->InputfieldFieldset;
$fieldset->label = $this->_('Visibility');
$fieldset->attr('name', 'visibility');
$fieldset->icon = 'eye';
$field = $inputfields->InputfieldSelect;
$field->attr('name', 'collapsed');
$field->label = $this->_('Presentation');
$field->icon = 'eye-slash';
$field->description = $this->_("How should this field be displayed in the editor?");
$field->addOption(self::collapsedNo, $this->_('Open'));
$field->addOption(self::collapsedNever, $this->_('Open + Cannot be closed'));
$field->addOption(self::collapsedNoLocked, $this->_('Open + Locked (not editable)'));
$field->addOption(self::collapsedBlank, $this->_('Open when populated + Closed when blank'));
if($this->hasFieldtype !== false) {
$field->addOption(self::collapsedBlankAjax, $this->_('Open when populated + Closed when blank + Load only when opened (AJAX)') . "");
if($this->collapsed == Inputfield::collapsedNo && !$this->getSetting('showIf')) {
$fieldset->collapsed = Inputfield::collapsedYes;
}
$field->addOption(self::collapsedBlankLocked, $this->_('Open when populated + Closed when blank + Locked (not editable)'));
$field->addOption(self::collapsedPopulated, $this->_('Open when blank + Closed when populated'));
$field->addOption(self::collapsedYes, $this->_('Closed'));
$field->addOption(self::collapsedYesLocked, $this->_('Closed + Locked (not editable)'));
$inputfields->append($fieldset);
$f = $inputfields->InputfieldSelect;
$f->attr('name', 'collapsed');
$f->label = $this->_('Presentation');
$f->icon = 'eye-slash';
$f->description = $this->_("How should this field be displayed in the editor?");
$f->addOption(self::collapsedNo, $this->_('Open'));
$f->addOption(self::collapsedNever, $this->_('Open + Cannot be closed'));
$f->addOption(self::collapsedNoLocked, $this->_('Open + Locked (not editable)'));
$f->addOption(self::collapsedBlank, $this->_('Open when populated + Closed when blank'));
if($this->hasFieldtype !== false) {
$field->addOption(self::collapsedYesAjax, $this->_('Closed + Load only when opened (AJAX)') . "");
$field->notes = sprintf($this->_('Options indicated with %s may not work with all input types or placements, test to ensure compatibility.'), '†');
$f->addOption(self::collapsedBlankAjax, $this->_('Open when populated + Closed when blank + Load only when opened (AJAX)') . "");
}
$field->addOption(self::collapsedHidden, $this->_('Hidden (not shown in the editor)'));
$field->attr('value', (int) $this->collapsed);
$fieldset->append($field);
$f->addOption(self::collapsedBlankLocked, $this->_('Open when populated + Closed when blank + Locked (not editable)'));
$f->addOption(self::collapsedPopulated, $this->_('Open when blank + Closed when populated'));
$f->addOption(self::collapsedYes, $this->_('Closed'));
$f->addOption(self::collapsedYesLocked, $this->_('Closed + Locked (not editable)'));
if($this->hasFieldtype !== false) {
$f->addOption(self::collapsedYesAjax, $this->_('Closed + Load only when opened (AJAX)') . "");
$f->notes = sprintf($this->_('Options indicated with %s may not work with all input types or placements, test to ensure compatibility.'), '†');
$f->addOption(self::collapsedTab, $this->_('Tab'));
$f->addOption(self::collapsedTabAjax, $this->_('Tab + Load only when clicked (AJAX)') . "");
$f->addOption(self::collapsedTabLocked, $this->_('Tab + Locked (not editable)'));
}
$f->addOption(self::collapsedHidden, $this->_('Hidden (not shown in the editor)'));
$f->attr('value', (int) $this->collapsed);
$fieldset->append($f);
$field = $inputfields->InputfieldText;
$field->label = $this->_('Show this field only if');
$field->description = $this->_('Enter the conditions under which the field will be shown.') . ' ' . $conditionsText;
$field->notes = $conditionsNote;
$field->icon = 'question-circle';
$field->attr('name', 'showIf');
$field->attr('value', $this->getSetting('showIf'));
$field->collapsed = Inputfield::collapsedBlank;
$field->showIf = "collapsed!=" . self::collapsedHidden;
$fieldset->append($field);
$fieldset->collapsed = $this->collapsed == Inputfield::collapsedNo && !$this->getSetting('showIf') ? Inputfield::collapsedYes : Inputfield::collapsedNo;
$inputfields->append($fieldset);
$f = $inputfields->InputfieldText;
$f->label = $this->_('Show this field only if');
$f->description = $this->_('Enter the conditions under which the field will be shown.') . ' ' . $conditionsText;
$f->notes = $conditionsNote;
$f->icon = 'question-circle';
$f->attr('name', 'showIf');
$f->attr('value', $this->getSetting('showIf'));
$f->collapsed = Inputfield::collapsedBlank;
$f->showIf = "collapsed!=" . self::collapsedHidden;
$fieldset->append($f);
$field = $inputfields->InputfieldInteger;
$value = (int) $this->getSetting('columnWidth');
$value = (int) $this->getSetting('columnWidth');
if($value < 10 || $value >= 100) $value = 100;
$field->label = sprintf($this->_('Column width (%d%%)'), $value);
$field->icon = 'arrows-h';
$field->attr('id+name', 'columnWidth');
$field->addClass('columnWidthInput');
$field->attr('type', 'text');
$field->attr('maxlength', 4);
$field->attr('size', 4);
$field->attr('max', 100);
$field->attr('value', $value . '%');
$field->description = $this->_("The percentage width of this field's container (10%-100%). If placed next to other fields with reduced widths, it will create floated columns."); // Description of colWidth option
$field->notes = $this->_("Note that not all fields will work at reduced widths, so you should test the result after changing this."); // Notes for colWidth option
if(!$this->wire('input')->get('process_template')) if($value == 100) $field->collapsed = Inputfield::collapsedYes;
$inputfields->append($field);
$f = $inputfields->InputfieldInteger;
$f->label = sprintf($this->_('Column width (%d%%)'), $value);
$f->icon = 'arrows-h';
$f->attr('id+name', 'columnWidth');
$f->addClass('columnWidthInput');
$f->attr('type', 'text');
$f->attr('maxlength', 4);
$f->attr('size', 4);
$f->attr('max', 100);
$f->attr('value', $value . '%');
$f->description = $this->_("The percentage width of this field's container (10%-100%). If placed next to other fields with reduced widths, it will create floated columns."); // Description of colWidth option
$f->notes = $this->_("Note that not all fields will work at reduced widths, so you should test the result after changing this."); // Notes for colWidth option
if(!$this->wire()->input->get('process_template') && $value == 100) $f->collapsed = Inputfield::collapsedYes;
$inputfields->append($f);
if(!$this instanceof InputfieldWrapper) {
$field = $inputfields->InputfieldCheckbox;
$field->label = $this->_('Required?');
$field->icon = 'asterisk';
$field->attr('name', 'required');
$field->attr('value', 1);
$field->attr('checked', $this->getSetting('required') ? 'checked' : '');
$field->description = $this->_("If checked, a value will be required for this field.");
$field->collapsed = $this->getSetting('required') ? Inputfield::collapsedNo : Inputfield::collapsedYes;
$inputfields->add($field);
$f = $inputfields->InputfieldCheckbox;
$f->label = $this->_('Required?');
$f->icon = 'asterisk';
$f->attr('name', 'required');
$f->attr('value', 1);
$f->attr('checked', $this->getSetting('required') ? 'checked' : '');
$f->description = $this->_("If checked, a value will be required for this field.");
$f->collapsed = $this->getSetting('required') ? Inputfield::collapsedNo : Inputfield::collapsedYes;
$inputfields->add($f);
$requiredAttr = $this->getSetting('requiredAttr');
if($requiredAttr !== null) {
// Inputfield must have set requiredAttr to some non-null value before this will appear as option in config
$field->columnWidth = 50; // required checkbox
$f->columnWidth = 50; // required checkbox
$f = $inputfields->InputfieldCheckbox;
$f->attr('name', 'requiredAttr');
$f->label = $this->_('Also use HTML5 “required” attribute?');
@@ -1530,16 +1738,36 @@ abstract class Inputfield extends WireData implements Module {
$inputfields->add($f);
}
$field = $inputfields->InputfieldText;
$field->label = $this->_('Required only if');
$field->icon = 'asterisk';
$field->description = $this->_('Enter the conditions under which a value will be required for this field.') . ' ' . $conditionsText;
$field->notes = $conditionsNote;
$field->attr('name', 'requiredIf');
$field->attr('value', $this->getSetting('requiredIf'));
$field->collapsed = $field->attr('value') ? Inputfield::collapsedNo : Inputfield::collapsedYes;
$field->showIf = "required>0";
$inputfields->add($field);
$f = $inputfields->InputfieldText;
$f->label = $this->_('Required only if');
$f->icon = 'asterisk';
$f->description = $this->_('Enter the conditions under which a value will be required for this field.') . ' ' . $conditionsText;
$f->notes = $conditionsNote;
$f->attr('name', 'requiredIf');
$f->attr('value', $this->getSetting('requiredIf'));
$f->collapsed = $f->attr('value') ? Inputfield::collapsedNo : Inputfield::collapsedYes;
$f->showIf = "required>0";
$inputfields->add($f);
}
if($this->hasFieldtype === false || $this->wire()->config->advanced) {
$f = $inputfields->InputfieldTextarea;
$f->attr('name', 'addClass');
$f->label = $this->_('Custom class attributes');
$f->description =
$this->_('Optionally add to the class attribute for specific elements in this Inputfield.') . ' ' .
$this->_('Format is one per line of `element:class` where `element` is one of: “wrap”, “header”, “content” or “input” and `class` is one or more class names.') . ' ' .
$this->_('If no element is specified then the “input” element is assumed.');
$f->notes = $this->_('Example:') . "`" .
"\nwrap:card card-default" .
"\nheader:card-header" .
"\ncontent:card-body" .
"\ninput:form-input input-checkbox" .
"`";
$f->collapsed = Inputfield::collapsedBlank;
$f->renderFlags = self::renderLast;
$f->val($this->getSetting('addClass'));
$inputfields->add($f);
}
return $inputfields;
@@ -1628,6 +1856,7 @@ abstract class Inputfield extends WireData implements Module {
$inputfields = $this->getConfigInputfields();
if(!$inputfields || !count($inputfields)) return $data;
foreach($inputfields->getAll() as $inputfield) {
/** @var Inputfield $inputfield */
$value = $inputfield->isEmpty() ? '' : $inputfield->value;
if(is_object($value)) $value = (string) $value;
$data[$inputfield->name] = $value;
@@ -1674,20 +1903,21 @@ abstract class Inputfield extends WireData implements Module {
*
* @param string $text Text of error message
* @param int $flags Optional flags
* @return mixed
* @return $this
*
*/
public function error($text, $flags = 0) {
// Override Wire's error method and place errors in the context of their inputfield
$session = $this->wire()->session;
$key = $this->getErrorSessionKey();
$errors = $this->wire('session')->$key;
$errors = $session->$key;
if(!is_array($errors)) $errors = array();
if(!in_array($text, $errors)) {
$errors[] = $text;
$this->wire('session')->set($key, $errors);
$session->set($key, $errors);
}
$label = $this->getSetting('label');
if(empty($label)) $label= $this->attr('name');
if(empty($label)) $label = $this->attr('name');
if(strlen($label)) $text .= " - $label";
return parent::error($text, $flags);
}
@@ -1705,16 +1935,31 @@ abstract class Inputfield extends WireData implements Module {
*
*/
public function getErrors($clear = false) {
$session = $this->wire()->session;
$key = $this->getErrorSessionKey();
$errors = $this->wire('session')->get($key);
$errors = $session->get($key);
if(!is_array($errors)) $errors = array();
if($clear) {
$this->wire('session')->remove($key);
$session->remove($key);
parent::errors("clear");
}
return $errors;
}
/**
* Clear errors from this Inputfield
*
* This is the same as `$inputfield->getErrors(true);` but has no return value.
*
* #pw-group-states
*
* @since 3.0.205
*
*/
public function clearErrors() {
$this->getErrors(true);
}
/**
* Does this Inputfield have the requested property or attribute?
*
@@ -1799,8 +2044,9 @@ abstract class Inputfield extends WireData implements Module {
*/
public function entityEncode($str, $markdown = false) {
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
$sanitizer = $this->wire()->sanitizer;
$str = (string) $str;
// if already encoded, then un-encode it
if(strpos($str, '&') !== false && preg_match('/&(#\d+|[a-zA-Z]+);/', $str)) {
@@ -1845,10 +2091,90 @@ abstract class Inputfield extends WireData implements Module {
*
*/
public function editable($setEditable = null) {
if(!is_null($setEditable)) $this->editable = $setEditable ? true : false;
if(!is_null($setEditable)) $this->editable = (bool) $setEditable;
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
@@ -25,6 +25,7 @@
*
* @method string renderInputfield(Inputfield $inputfield, $renderValueMode = false) #pw-group-output
* @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
@@ -98,7 +99,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
'item_icon' => "<i class='fa fa-fw fa-{name}'></i> ",
'item_toggle' => "<i class='toggle-icon fa fa-fw fa-angle-down' data-to='fa-angle-down fa-angle-right'></i>",
// ALSO:
// InputfieldAnything => array( any of the properties above to override on a per-Inputifeld basis)
// InputfieldAnything => array(any of the properties above to override on a per-Inputfield basis)
);
static protected $markup = array();
@@ -122,7 +123,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
'item_show_if' => 'InputfieldStateShowIf',
'item_required_if' => 'InputfieldStateRequiredIf'
// ALSO:
// InputfieldAnything => array( any of the properties above to override on a per-Inputifeld basis)
// InputfieldAnything => array(any of the properties above to override on a per-Inputfield basis)
);
static protected $classes = array();
@@ -184,7 +185,6 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$columnWidthSpacing = is_null($columnWidthSpacing) ? 1 : (int) $columnWidthSpacing;
if($columnWidthSpacing > 0) $this->set('columnWidthSpacing', $columnWidthSpacing);
$columnWidthSpacing = null;
$settings = $config->InputfieldWrapper;
if(is_array($settings)) {
@@ -245,7 +245,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
if(strpos($key, 'Inputfield') === 0 && strlen($key) > 10) {
if($key === 'InputfieldWrapper') return $this->wire(new InputfieldWrapper());
$value = $this->wire()->modules->get($key);
if($value && $value instanceof Inputfield) return $value;
if($value instanceof Inputfield) return $value;
if(wireClassExists($key)) return $this->wire(new $key());
$value = null;
}
@@ -318,7 +318,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$inputfield = $this->wire(new $typeName());
}
if(!$inputfield || !$inputfield instanceof Inputfield) {
if(!$inputfield instanceof Inputfield) {
throw new WireException("Unknown Inputfield type: $typeName");
}
@@ -402,6 +402,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).
@@ -431,10 +433,11 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$f = $this->getByName($item['name']);
if($f) return $this->insert($f, $existingItem, $before);
}
$n = $children->count();
$nBefore = $children->count();
$this->add($item);
if($children->count() > $n) {
// new item was added
$nAfter = $children->count();
if($nAfter > $nBefore) {
// new item was added by the above $this->add() call
$item = $children->last();
$children->remove($item);
} else {
@@ -461,12 +464,11 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
if($f && $f->parent) {
// existing item was found
$existingItem = $f;
$existingItem->parent->insert($item, $existingItem, $before);
} else {
// existing item not found, add it as direct child
$this->add($existingItem);
$existingItem->parent->insert($item, $existingItem, $before);
}
$existingItem->parent->insert($item, $existingItem, $before);
}
return $this;
@@ -535,11 +537,12 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
*
* #pw-group-manipulation
*
* @param Inputfield|string $item Inputfield object or name
* @param Inputfield|string $key Inputfield object or name
* @return $this
*
*/
public function remove($item) {
public function remove($key) {
$item = $key;
if(!$item) return $this;
if(!$item instanceof Inputfield) {
if(!is_string($item)) return $this;
@@ -548,7 +551,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
}
if($this->children()->has($item)) {
$this->children()->remove($item);
} if($this->getChildByName($item->attr('name')) && $item->parent) {
} else if($this->getChildByName($item->attr('name')) && $item->parent) {
$item->parent->remove($item);
}
return $this;
@@ -564,6 +567,9 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$children = $this->wire(new InputfieldWrapper());
$wrappers = array($children);
$prepend = array();
$append = array();
$numMove = 0;
foreach($this->children() as $inputfield) {
@@ -575,11 +581,31 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
} else if($inputfield instanceof InputfieldFieldsetOpen) {
$inputfield->set('InputfieldWrapper_isPreRendered', true);
array_push($wrappers, $inputfield);
$wrappers[] = $inputfield;
}
$inputfield->unsetParent();
$wrapper->add($inputfield);
$wrapper->add($inputfield);
$flags = $inputfield->renderFlags;
if($flags & Inputfield::renderFirst) {
$prepend[] = $inputfield;
$numMove++;
} else if($flags & Inputfield::renderLast) {
$append[] = $inputfield;
$numMove++;
}
}
if($numMove) {
foreach($prepend as $f) {
/** @var Inputfield $f */
$f->getParent()->prepend($f);
}
foreach($append as $f) {
/** @var Inputfield $f */
$f->getParent()->append($f);
}
}
return $children;
@@ -697,7 +723,14 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$classes = array();
$useColumnWidth = $this->useColumnWidth;
$renderAjaxInputfield = $this->wire()->config->ajax ? $this->wire()->input->get('renderInputfieldAjax') : null;
$lockedStates = array(Inputfield::collapsedNoLocked, Inputfield::collapsedYesLocked, Inputfield::collapsedBlankLocked);
$toggleLabel = $sanitizer->entities1($this->_('Toggle open/close'));
$lockedStates = array(
Inputfield::collapsedNoLocked,
Inputfield::collapsedYesLocked,
Inputfield::collapsedBlankLocked,
Inputfield::collapsedTabLocked
);
if($useColumnWidth === true && isset($_classes['form']) && strpos($_classes['form'], 'InputfieldFormNoWidths') !== false) {
$useColumnWidth = false;
@@ -710,6 +743,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
}
foreach($children as $inputfield) {
/** @var Inputfield $inputfield */
if($renderAjaxInputfield && $inputfield->attr('id') !== $renderAjaxInputfield
&& !$inputfield instanceof InputfieldWrapper) {
@@ -744,7 +778,8 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$errors = $inputfield->getErrors(true);
if(count($errors)) {
$collapsed = $renderValueMode ? Inputfield::collapsedNoLocked : Inputfield::collapsedNo;
$errorsOut = implode(', ', $errors);
$comma = $this->_(','); // Comma or other character to separate multiple error messages
$errorsOut = implode("$comma ", $errors);
}
} else $errors = array();
@@ -831,7 +866,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
// wrap the inputfield output
$attrs = '';
$label = $inputfield->getSetting('label');
$label = (string) $inputfield->getSetting('label');
$skipLabel = $inputfield->getSetting('skipLabel');
$skipLabel = is_bool($skipLabel) || empty($skipLabel) ? (bool) $skipLabel : (int) $skipLabel; // force as bool or int
if(!strlen($label) && $skipLabel !== Inputfield::skipLabelBlank && $inputfield->className() != 'InputfieldWrapper') {
@@ -851,25 +886,23 @@ 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']);
} 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->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 = '';
@@ -907,9 +940,15 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
if(!isset($ffAttrs['id'])) $ffAttrs['id'] = 'wrap_' . $inputfield->attr('id');
$ffAttrs['class'] = str_replace('Inputfield_ ', '', $ffAttrs['class']);
$wrapClass = $inputfield->getSetting('wrapClass');
if($wrapClass) $ffAttrs['class'] .= " " . $wrapClass;
$fieldName = $inputfield->attr('data-field-name');
if($fieldName && $fieldName != $inputfield->attr('name')) {
// ensures that Inputfields renamed by context retain the original field-name based class
$wrapClass = "Inputfield_$fieldName $wrapClass";
if(!isset($ffAttrs['data-id'])) $ffAttrs['data-id'] = "wrap_Inputfield_$fieldName";
}
if($wrapClass) $ffAttrs['class'] = trim("$ffAttrs[class] $wrapClass");
foreach($inputfield->wrapAttr() as $k => $v) {
if(!empty($ffAttrs[$k])) {
if($k === 'class' && !empty($ffAttrs[$k])) {
$ffAttrs[$k] .= " $v";
} else {
$ffAttrs[$k] = $v;
@@ -922,16 +961,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)
@@ -954,6 +986,115 @@ 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;
}
/**
* 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 = $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)
*
@@ -988,8 +1129,9 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
public function ___renderInputfield(Inputfield $inputfield, $renderValueMode = false) {
$inputfieldID = $inputfield->attr('id');
$collapsed = $inputfield->getSetting('collapsed');
$ajaxInputfield = $collapsed == Inputfield::collapsedYesAjax || ($collapsed == Inputfield::collapsedBlankAjax && $inputfield->isEmpty());
$collapsed = (int) $inputfield->getSetting('collapsed');
$ajaxInputfield = $collapsed == Inputfield::collapsedYesAjax || $collapsed === Inputfield::collapsedTabAjax
|| ($collapsed == Inputfield::collapsedBlankAjax && $inputfield->isEmpty());
$ajaxHiddenInput = "<input type='hidden' name='processInputfieldAjax[]' value='$inputfieldID' />";
$ajaxID = $this->wire()->config->ajax ? $this->wire()->input->get('renderInputfieldAjax') : '';
$required = $inputfield->getSetting('required');
@@ -1000,6 +1142,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$ajaxInputfield = false;
if($collapsed == Inputfield::collapsedYesAjax) $inputfield->collapsed = Inputfield::collapsedYes;
if($collapsed == Inputfield::collapsedBlankAjax) $inputfield->collapsed = Inputfield::collapsedBlank;
if($collapsed == Inputfield::collapsedTabAjax) $inputfield->collapsed = Inputfield::collapsedTab;
// indicate to next processInput that this field can be processed
$inputfield->appendMarkup .= $ajaxHiddenInput;
}
@@ -1092,11 +1235,27 @@ 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
foreach($inputfield->getAll() as $in) {
/** @var Inputfield $in */
$in->renderReady($inputfield, $renderValueMode);
}
}
@@ -1120,12 +1279,17 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
*/
public function ___processInput(WireInputData $input) {
if(!$this->children) return $this;
if(!$this->children) return $this;
$hasHook = $this->isHooked('InputfieldWrapper::allowProcessInput()');
foreach($this->children() as $key => $child) {
foreach($this->children() as $child) {
/** @var Inputfield $child */
// skip over the field if it is not processable
// skip over the inputfield if hook tells us so
if($hasHook && !$this->allowProcessInput($child)) continue;
// skip over the inputfield if it is not processable
if(!$this->isProcessable($child)) continue;
// pass along the dependencies value to child wrappers
@@ -1172,12 +1336,20 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
Inputfield::collapsedLocked,
Inputfield::collapsedNoLocked,
Inputfield::collapsedBlankLocked,
Inputfield::collapsedYesLocked
Inputfield::collapsedYesLocked,
Inputfield::collapsedTabLocked,
);
$ajaxTypes = array(
Inputfield::collapsedYesAjax,
Inputfield::collapsedBlankAjax,
Inputfield::collapsedTabAjax,
);
$collapsed = (int) $inputfield->getSetting('collapsed');
if(in_array($collapsed, $skipTypes)) return false;
if(in_array($collapsed, array(Inputfield::collapsedYesAjax, Inputfield::collapsedBlankAjax))) {
if(in_array($collapsed, $ajaxTypes)) {
$processAjax = $this->wire()->input->post('processInputfieldAjax');
if(is_array($processAjax) && in_array($inputfield->attr('id'), $processAjax)) {
// field can be processed (convention used by InputfieldWrapper)
@@ -1211,6 +1383,25 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
return true;
}
/**
* Allow input to be processed for given Inputfield? (for hooks)
*
* IMPORTANT: This method is not called unless it is hooked! Descending classes
* should instead implement the isProcessable() method (when needed) and be sure to
* call the parent isProcessable() method too.
*
* #pw-hooker
* #pw-internal
*
* @param Inputfield $inputfield
* @return bool
* @since 3.0.207
*
*/
public function ___allowProcessInput(Inputfield $inputfield) {
return true;
}
/**
* Returns true if all children are empty, or false if one or more is populated
*
@@ -1222,6 +1413,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
public function isEmpty() {
$empty = true;
foreach($this->children() as $child) {
/** @var Inputfield $child */
if(!$child->isEmpty()) {
$empty = false;
break;
@@ -1245,6 +1437,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$a = array();
static $n = 0;
foreach($this->children() as $child) {
/** @var Inputfield $child */
if($child instanceof InputfieldWrapper) {
$a = array_merge($a, $child->getEmpty($required));
} else {
@@ -1263,8 +1456,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
*
* Should only be called after `InputfieldWrapper::processInput()`.
*
* #pw-group-input
* #pw-group-retrieval-and-traversal
* #pw-group-errors
*
* @param bool $clear Specify true to clear out the errors (default=false).
* @return array Array of error strings
@@ -1272,7 +1464,8 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
*/
public function getErrors($clear = false) {
$errors = parent::getErrors($clear);
foreach($this->children() as $key => $child) {
foreach($this->children() as $child) {
/** @var Inputfield $child */
foreach($child->getErrors($clear) as $e) {
$label = $child->getSetting('label');
$msg = $label ? $label : $child->attr('name');
@@ -1282,6 +1475,33 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
return $errors;
}
/**
* Get Inputfield objects that have errors
*
* #pw-group-errors
*
* @return array|Inputfield[] Array of Inputfield objects indexed by Inputfield name attribute
* @since 3.0.205
*
*/
public function getErrorInputfields() {
$a = array();
if(count(parent::getErrors())) {
$name = $this->attr('name');
$a[$name] = $this;
}
foreach($this->children() as $child) {
/** @var Inputfield $child */
if($child instanceof InputfieldWrapper) {
$a = array_merge($a, $child->getErrorInputfields());
} else if(count($child->getErrors())) {
$name = $child->attr('name');
$a[$name] = $child;
}
}
return $a;
}
/**
* Return all children Inputfield objects
*
@@ -1333,6 +1553,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
// child by name
$wrappers = array();
foreach($children as $f) {
/** @var Inputfield $f */
if($f->getAttribute('name') === $name) {
$child = $f;
break;
@@ -1428,7 +1649,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
* #pw-group-retrieval-and-traversal
*
* @param string $name
* @return Inputfield|null
* @return Inputfield|InputfieldWrapper|null
* @since 3.0.172
*
*/
@@ -1445,13 +1666,14 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
*
* @param string $attrName Attribute to match, such as 'id', 'name', 'value', etc.
* @param string $attrValue Attribute value to match
* @return Inputfield|null
* @return Inputfield|InputfieldWrapper|null
* @since 3.0.196
*
*/
public function getByAttr($attrName, $attrValue) {
$inputfield = null;
foreach($this->children() as $child) {
/** @var Inputfield $child */
if($child->getAttribute($attrName) === $attrValue) {
$inputfield = $child;
} else if($child instanceof InputfieldWrapper) {
@@ -1462,6 +1684,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
*
@@ -1470,7 +1750,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
* Inputfield cannot be found.
*
* @param string $name
* @return string|int|float|bool|array|object|null
* @return array|float|int|object|Wire|WireArray|WireData|string|null
* @since 3.0.172
*
*/
@@ -1516,7 +1796,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
*
* @param array $options Options to modify behavior (3.0.169+)
* - `withWrappers` (bool): Also include InputfieldWrapper objects? (default=false) 3.0.169+
* @return InputfieldWrapper|InputfieldsArray
* @return InputfieldsArray
*
*/
public function getAll(array $options = array()) {
@@ -1580,11 +1860,20 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
/** @var InputfieldSelect $f */
$f = $inputfields->getChildByName('collapsed');
if($f) {
// remove all options for 'collapsed' except collapsedYes and collapsedNo
// 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, array(Inputfield::collapsedNo, Inputfield::collapsedYes))) {
$f->removeOption($value);
}
if(!in_array($value, $allow)) $f->removeOption($value);
}
}
@@ -1689,11 +1978,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;
@@ -1783,6 +2072,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
public function populateValues($data) {
$populated = array();
foreach($this->getAll() as $inputfield) {
/** @var Inputfield $inputfield */
if($inputfield instanceof InputfieldWrapper) continue;
$name = $inputfield->attr('name');
if(!$name) continue;
@@ -1816,6 +2106,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
public function debugMap() {
$a = array();
foreach($this as $in) {
/** @var Inputfield $in */
$info = array(
'id' => $in->id,
'name' => $in->name,
@@ -1844,4 +2135,3 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
}
}

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

@@ -202,6 +202,91 @@ interface FieldtypeHasFiles {
public function getFilesPath(Page $page, Field $field);
}
/**
* Indicates Fieldtype manages Pagefile/Pageimage objects
*
*/
interface FieldtypeHasPagefiles {
/**
* Get Pagefiles
*
* @param Page $page
* @param Field $field
* @return Pagefiles|Pagefile[]
*
*/
public function getPagefiles(Page $page, Field $field);
}
/**
* Indicates Fieldtype manages Pageimage objects
*
*/
interface FieldtypeHasPageimages {
/**
* Get Pageimages
*
* @param Page $page
* @param Field $field
* @return Pageimages|Pageimage[]
*
*/
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
@@ -359,10 +444,25 @@ interface WireDatabase {
interface WirePageEditor {
/**
* @return Page The current page being edited
*
*/
public function getPage();
}
/**
* Interface indicates item stores in a WireArray or type descending from it
*
* @since 3.0.205
*
*/
interface WireArrayItem {
/**
* @return WireArray
*
*/
public function getWireArray();
}
/**
* Interface shared by all ProcessWire Null objects
*
@@ -471,7 +571,7 @@ interface LanguagesValueInterface {
* Given a language, returns the value in that language
*
* @param Language|int
* @return int
* @return string|mixed
*
*/
public function getLanguageValue($languageID);
@@ -658,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
@@ -672,4 +772,103 @@ interface InputfieldHasSelectableOptions {
public function addOptionLabel($value, $label, $language = null);
}
/**
* Interface for WireCache handler classes
*
* For example implementations of this interface see
* WireCacheDatabase (core) and WireCacheFilesystem (module)
*
* @since 3.0.218
*
*/
interface WireCacheInterface {
/**
* Find caches by names and/or expirations and return requested values
*
* ~~~~~
* // Default options
* $defaults = [
* 'names' => [],
* 'expires' => [],
* 'expiresMode' => 'OR',
* 'get' => [ 'name', 'expires', 'data' ],
* ];
*
* // Example options
* $options['names'] = [ 'my-cache', 'your-cache', 'hello-*' ];
* $options['expires'] => [
* '<= ' . WireCache::expiresNever,
* '>= ' . date('Y-m-d H:i:s')
* ];
* ~~~~~
*
* @param array $options
* - `get` (array): Properties to get in return value, one or more of [ `name`, `expires`, `data`, `size` ] (default=all)
* - `names` (array): Names of caches to find (OR condition), optionally appended with wildcard `*`.
* - `expires` (array): Expirations of caches to match in ISO-8601 date format, prefixed with operator and space (see expiresMode mode below).
* - `expiresMode` (string): Whether it should match any one condition 'OR', or all conditions 'AND' (default='OR')
* @return array Returns array of associative arrays, each containing requested properties
*
*/
public function find(array $options);
/**
* Save a cache
*
* @param string $name
* @param string $data
* @param string $expire
* @return bool
*
*/
public function save($name, $data, $expire);
/**
* Delete cache by name
*
* @param string $name
* @return bool
*
*/
public function delete($name);
/**
* Delete all caches (except those reserved by the system)
*
* @return int
*
*/
public function deleteAll();
/**
* Expire all caches (except those that should never expire)
*
* @return int
*
*/
public function expireAll();
/**
* Optional method to perform maintenance
*
* When present, this method should return true if it handled maintenance or false if it did not.
* If it returns false, WireCache will attempt to perform maintenance instead by calling find and
* delete methods where appropriate.
*
* WireCache passes either null, a Page object or a Template object as the single argument.
* When null is passed, it means "general maintenance". When a Page or Template object is
* passed then it means that the given Page or Template was just saved, and to perform any
* necessary maintenance for that case. If the method handles general maintenance but not
* object maintenance, then it should return true when it receives null, and false when it
* receives a Page or Template.
*
* @param Page|Template|null $obj
* @return bool
* @since 3.0.219
*
* The method below is commented out because it optional and only used only if present:
*
*/
// public function maintenance($obj = null);
}

View File

@@ -284,7 +284,14 @@ function _x($text, $context, $textdomain = null) {
*
*/
function _n($textSingular, $textPlural, $count, $textdomain = null) {
return $count == 1 ? __($textSingular, $textdomain) : __($textPlural, $textdomain);
$count = (int) $count;
if($count === 0 && wire()->languages) {
$plural = __('0-plural', 'common');
$value = strtolower($plural) === '0-singular' ? __($textSingular, $textdomain) : __($textPlural, $textdomain);
} else {
$value = $count === 1 ? __($textSingular, $textdomain) : __($textPlural, $textdomain);
}
return $value;
}
/**
@@ -423,5 +430,3 @@ function wireLangTranslations(array $values = array()) {
function wireLangReplacements(array $values) {
return __(true, 'replacements', $values);
}

View File

@@ -74,12 +74,13 @@ 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);
if(!is_null($value)) $this->setValue($value);
@@ -112,7 +113,7 @@ class MarkupFieldtype extends WireData implements Module {
$valid = false;
if($value instanceof PageArray) {
// PageArray object: get array of property value from each item
$field = $this->wire('fields')->get($property);
$field = $this->wire()->fields->get($property);
if(is_object($field) && $field->type) {
$a = array();
foreach($value as $page) {
@@ -126,7 +127,8 @@ class MarkupFieldtype extends WireData implements Module {
}
return $this->arrayToString($a, false);
} else {
$value = $value->explode($property, array('getMethod' => 'getFormatted'));
$getMethod = strpos($property, '}') ? 'getText' : 'getFormatted';
$value = $value->explode($property, array('getMethod' => $getMethod));
}
$valid = true;
@@ -139,25 +141,21 @@ class MarkupFieldtype extends WireData implements Module {
// Page object
$page = $value;
$value = $page->getFormatted($property);
$field = $this->wire('fields')->get($property);
$field = $this->wire()->fields->get($property);
if(is_object($field) && $field->type) return $field->type->markupValue($page, $field, $value);
$valid = true;
} else if($value instanceof LanguagesValueInterface) {
/** @var LanguagesValueInterface $value */
/** @var Languages $languages */
$languages = $this->wire('languages');
if($property) {
if($property === 'data') {
$languageID = $languages->getDefault()->id;
} else if(is_string($property) && preg_match('/^data(\d+)$/', $property, $matches)) {
$languageID = (int) $matches[1];
} else {
$languageID = 0;
}
$value = $languageID ? $value->getLanguageValue($languageID) : (string) $value;
$languages = $this->wire()->languages;
if($property === 'data') {
$languageID = $languages->getDefault()->id;
} else if(is_string($property) && preg_match('/^data(\d+)$/', $property, $matches)) {
$languageID = (int) $matches[1];
} else {
$value = (string) $value;
$languageID = 0;
}
$value = $languageID ? $value->getLanguageValue($languageID) : (string) $value;
} else if($value instanceof WireData) {
// WireData object
@@ -268,14 +266,17 @@ class MarkupFieldtype extends WireData implements Module {
*
*/
protected function valueToString($value, $encode = true) {
if(is_object($value) && ($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;
return $encode ? $this->wire()->sanitizer->entities1($value) : $value;
}
}
@@ -305,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');
@@ -387,12 +393,12 @@ class MarkupFieldtype extends WireData implements Module {
*
*/
public function __toString() {
return $this->render();
return (string) $this->render();
}
public function setPage(Page $page) { $this->_page = $page; }
public function setField(Field $field) { $this->_field = $field; }
public function getPage() { return $this->_page ? $this->_page : $this->wire('pages')->newNullPage(); }
public function getPage() { return $this->_page ? $this->_page : $this->wire()->pages->newNullPage(); }
public function getField() { return $this->_field; }
/**

View File

@@ -17,7 +17,7 @@
*
* Runtime errors are logged to: /site/assets/logs/markup-qa-errors.txt
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
@@ -67,11 +67,12 @@ 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);
$page->wire($this);
@@ -80,8 +81,7 @@ class MarkupQA extends Wire {
$this->setField($field);
if(!$page) $field->wire($this);
}
/** @var Config $config */
$config = $this->wire('config');
$config = $this->wire()->config;
$this->assetsURL = $config->urls->assets;
$settings = $config->markupQA;
if(is_array($settings) && count($settings)) {
@@ -156,7 +156,7 @@ class MarkupQA extends Wire {
public function debug($set = null) {
if(is_bool($set)) {
if($set === true) {
$user = $this->wire('user');
$user = $this->wire()->user;
if(!$user || !$user->isSuperuser()) $set = false;
}
$this->settings['debug'] = $set;
@@ -213,7 +213,7 @@ class MarkupQA extends Wire {
// see if quick exit possible
if(stripos($value, 'href=') === false && stripos($value, 'src=') === false) return;
$config = $this->wire('config');
$config = $this->wire()->config;
$httpHost = $config->httpHost;
$rootURL = $config->urls->root;
$rootHostURL = $httpHost . $rootURL;
@@ -241,7 +241,7 @@ class MarkupQA extends Wire {
if($this->verbose()) {
$info = $this->page->get('_markupQA');
if(!is_array($info)) $info = array();
if(!is_array($info[$this->field->name])) $info[$this->field->name] = array();
if(!isset($info[$this->field->name]) || !is_array($info[$this->field->name])) $info[$this->field->name] = array();
$info[$this->field->name]['href'] = substr_count($value, "\thref=");
$info[$this->field->name]['src'] = substr_count($value, "\tsrc=");
$this->page->setQuietly('_markupQA', $info);
@@ -326,19 +326,20 @@ class MarkupQA extends Wire {
$info = $this->verbose() ? $this->page->get('_markupQA') : array();
if(!is_array($info)) $info = array();
$counts = array(
'external' => 0,
'internal' => 0,
'relative' => 0,
'files' => 0,
'other' => 0,
'unresolved' => 0,
'nohttp' => 0,
'ignored' => 0,
);
if(isset($info[$this->field->name])) {
$counts = $info[$this->field->name];
} else {
$counts = array(
'external' => 0,
'internal' => 0,
'relative' => 0,
'files' => 0,
'other' => 0,
'unresolved' => 0,
'nohttp' => 0,
'ignored' => 0,
);
$counts = array_merge($counts, $info[$this->field->name]);
}
$re = '!' .
@@ -393,7 +394,7 @@ class MarkupQA extends Wire {
} else if(strrpos($path, '.') > strrpos($path, '/')) {
// not relative and possibly a filename
// if this link is to a file that exists, then it's not a page link so skip it
$file = $this->wire('config')->paths->root . ltrim($path, '/');
$file = $config->paths->root . ltrim($path, '/');
if(file_exists($file)) {
$counts['files']++;
continue;
@@ -453,14 +454,14 @@ class MarkupQA extends Wire {
}
} else {
// did not resolve to a page, see if it resolves to a file or directory
$file = $this->wire('config')->paths->root . ltrim($path, '/');
$file = $config->paths->root . ltrim($path, '/');
if(file_exists($file)) {
if($debug) $this->message("MarkupQA sleepLinks link resolved to a file: $path");
$counts['files']++;
} else {
$parts = explode('/', trim($path, '/'));
$firstPart = array_shift($parts);
$test = $this->wire('config')->paths->root . $firstPart;
$test = $config->paths->root . $firstPart;
if(is_dir($test)) {
// possibly to something in another application, i.e. processwire.com/talk/
$counts['other']++;
@@ -506,9 +507,10 @@ class MarkupQA extends Wire {
if(!preg_match_all($re, $value, $matches)) return array();
$replacements = array();
$languages = $this->wire('languages');
$rootURL = $this->wire('config')->urls->root;
$adminURL = $this->wire('config')->urls->admin;
$languages = $this->wire()->languages;
$config = $this->wire()->config;
$rootURL = $config->urls->root;
$adminURL = $config->urls->admin;
$adminPath = $rootURL === '/' ? $adminURL : str_replace($rootURL, '/', $adminURL);
$debug = $this->debug();
@@ -539,10 +541,8 @@ class MarkupQA extends Wire {
} else {
$language = null;
}
$livePath = $this->wire('pages')->getPath($pageID, array(
'language' => $language
));
$livePath = $this->getPagePathFromId($pageID, $language);
if($urlSegmentStr) {
$livePath = rtrim($livePath, '/') . "/$urlSegmentStr";
@@ -606,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
@@ -617,7 +617,11 @@ 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;
$database = $this->wire()->database;
$defaults = array(
'getIDs' => false,
@@ -632,7 +636,7 @@ class MarkupQA extends Wire {
} else if($options['getCount']) {
$result = 0;
} else {
$result = $this->wire('pages')->newPageArray();
$result = $pages->newPageArray();
}
if(!$page) $page = $this->page;
@@ -652,11 +656,11 @@ class MarkupQA extends Wire {
// find pages
if($options['getCount'] && !$options['confirm']) {
// just return a count
return $this->wire('pages')->count($selector);
return $pages->count($selector);
} else {
// find the IDs
$checkIDs = array();
$foundIDs = $this->wire('pages')->findIDs($selector);
$foundIDs = $pages->findIDs($selector);
if(!count($foundIDs)) return $result;
if($options['confirm']) {
$checkIDs = array_flip($foundIDs);
@@ -667,12 +671,12 @@ class MarkupQA extends Wire {
// confirm results
foreach($fieldNames as $fieldName) {
if(!count($checkIDs)) break;
$field = $this->wire('fields')->get($fieldName);
$field = $fields->get($fieldName);
if(!$field) continue;
$table = $field->getTable();
$ids = implode(',', array_keys($checkIDs));
$sql = "SELECT * FROM `$table` WHERE `pages_id` IN($ids)";
$query = $this->wire('database')->prepare($sql);
$query = $database->prepare($sql);
$query->execute();
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
@@ -698,7 +702,7 @@ class MarkupQA extends Wire {
} else if($options['getCount']) {
$result = count($foundIDs);
} else {
$result = $this->wire('pages')->getById($foundIDs);
$result = $pages->getById($foundIDs);
}
}
@@ -713,7 +717,7 @@ class MarkupQA extends Wire {
*
*/
protected function linkWarning($path, $logWarning = true) {
if($this->wire('page')->template == 'admin' && $this->wire('process') == 'ProcessPageEdit') {
if($this->wire()->page->template == 'admin' && $this->wire()->process == 'ProcessPageEdit') {
$this->warning(sprintf(
$this->_('Unable to resolve link on page %1$s in field "%2$s": %3$s'),
$this->page->path,
@@ -738,7 +742,7 @@ class MarkupQA extends Wire {
*/
public function checkImgTags(&$value, array $options = array()) {
if(strpos($value, '<img ') !== false && preg_match_all('{(<' . 'img [^>]+>)}', $value, $matches)) {
foreach($matches[0] as $key => $img) {
foreach($matches[0] as $img) {
$this->checkImgTag($value, $img, $options);
}
}
@@ -784,7 +788,7 @@ class MarkupQA extends Wire {
if(!isset($info['img_noalt'])) $info['img_noalt'] = 0; // blank alt
// determine current 'alt' and 'src' attributes
foreach($attrStrings as $n => $attr) {
foreach($attrStrings as $attr) {
if(!strpos($attr, '=')) continue;
list($name, $val) = explode('=', $attr);
@@ -868,7 +872,7 @@ class MarkupQA extends Wire {
if($this->page->of()) {
$alt = $pagefile->description;
if(strlen($alt)) {
$alt = $this->wire('sanitizer')->entities1($alt);
$alt = $this->wire()->sanitizer->entities1($alt);
$_img = str_replace(" $replaceAlt", " alt=\"$alt\"", $img);
$value = str_replace($img, $_img, $value);
}
@@ -896,6 +900,7 @@ class MarkupQA extends Wire {
*
*/
protected function checkImgExists(Pageimage $pagefile, $img, $src, &$value) {
$basename = basename($src);
$pathname = $pagefile->pagefiles->path() . $basename;
@@ -928,6 +933,7 @@ class MarkupQA extends Wire {
$good = 0;
$bad = 0;
$debug = $this->debug() || $this->wire()->config->debug;
foreach(array_reverse($variations) as $info) {
// definitely a variation, attempt to re-create it
@@ -937,14 +943,13 @@ class MarkupQA extends Wire {
$options['suffix'] = $info['suffix'];
if(in_array('hidpi', $options['suffix'])) $options['hidpi'] = true;
}
/** @var Pageimage $newPagefile */
$newPagefile = $pagefile->size($info['width'], $info['height'], $options);
if($newPagefile && is_file($newPagefile->filename())) {
if(!empty($info['targetName']) && $newPagefile->basename != $info['targetName']) {
// new name differs from what is in text. Rename file to be consistent with text.
rename($newPagefile->filename(), $pathname);
}
if($this->debug() || $this->wire('config')->debug) {
if($debug) {
$this->message($this->_('Re-created image variation') . " - $newPagefile->name");
}
$pagefile = $newPagefile; // for next iteration
@@ -971,7 +976,7 @@ class MarkupQA extends Wire {
*/
public function error($text, $flags = 0) {
$logText = "$text (field={$this->field->name}, id={$this->page->id}, path={$this->page->path})";
$this->wire('log')->save(self::errorLogName, $logText);
$this->wire()->log->save(self::errorLogName, $logText);
/*
if($this->wire('modules')->isInstalled('SystemNotifications')) {
$user = $this->wire('modules')->get('SystemNotifications')->getSystemUser();
@@ -1018,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

@@ -1,7 +1,7 @@
<?php namespace ProcessWire;
/**
* Class ModuleConfig
* ModuleConfig class
*
* Serves as the base for classes dedicated to configuring modules.
*
@@ -11,6 +11,9 @@
* This file is licensed under the MIT license
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class ModuleConfig extends WireData {
@@ -46,6 +49,7 @@ class ModuleConfig extends WireData {
*
*/
public function __construct() {
parent::__construct();
}
/**
@@ -95,7 +99,7 @@ class ModuleConfig extends WireData {
* Set an array that defines Inputfields
*
* @param array $a
* @return this
* @return self
*
*/
public function add(array $a) {

View File

@@ -10,7 +10,7 @@
*
* See the Module interface (Module.php) for details about each method.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* This file is licensed under the MIT license
@@ -33,7 +33,7 @@ abstract class ModuleJS extends WireData implements Module {
'summary' => '', // 1 sentence summary of module
'href' => '', // URL to more information (optional)
'permanent' => false, // true if module is permanent and thus not uninstallable
);
);
}
@@ -109,24 +109,24 @@ abstract class ModuleJS extends WireData implements Module {
public function init() {
$class = $this->className();
$config = $this->wire('config');
$config = $this->wire()->config;
$version = $config->version;
$debug = $config->debug;
$file = $config->paths->$class . "$class.css";
if($this->loadStyles && is_file($file)) {
$mtime = filemtime($file);
$this->config->styles->add($config->urls->$class . "$class.css?v=$mtime");
if($debug) $version = filemtime($file);
$this->config->styles->add($config->urls->$class . "$class.css?v=$version");
}
$file = $config->paths->$class . "$class.js";
$mtime = 0;
if($this->loadScripts && is_file($file)) {
$minFile = $config->paths->$class . "$class.min.js";
if(!$config->debug && is_file($minFile)) {
$mtime = filemtime($minFile);
$config->scripts->add($config->urls->$class . "$class.min.js?v=$mtime");
if(!$debug && is_file($minFile)) {
$config->scripts->add($config->urls->$class . "$class.min.js?v=$version");
} else {
$mtime = filemtime($file);
$config->scripts->add($config->urls->$class . "$class.js?v=$mtime");
if($debug) $version = filemtime($file);
$config->scripts->add($config->urls->$class . "$class.js?v=$version");
}
}
@@ -134,11 +134,11 @@ abstract class ModuleJS extends WireData implements Module {
foreach($this->requested as $name) {
$url = $this->components[$name];
if(strpos($url, '/') === false) {
$mtime = filemtime($config->paths->$class . $url);
if($debug) $version = filemtime($config->paths->$class . $url);
$url = $config->urls->$class . $url;
}
$url .= "?v=$mtime";
$this->wire('config')->scripts->add($url);
$url .= "?v=$version";
$config->scripts->add($url);
}
$this->requested = array();
}
@@ -155,9 +155,10 @@ abstract class ModuleJS extends WireData implements Module {
*/
public function ___use($name) {
$name = $this->wire('sanitizer')->name($name);
$class = $this->className();
$config = $this->wire('config');
$config = $this->wire()->config;
if(!ctype_alnum($name)) $name = $this->wire()->sanitizer->name($name);
if(!isset($this->components[$name])) {
$this->error("Unrecognized $class component requested: $name");
@@ -166,13 +167,13 @@ abstract class ModuleJS extends WireData implements Module {
if($this->initialized) {
$url = $this->components[$name];
$mtime = 0;
$version = $config->version;
if(strpos($url, '/') === false) {
$file = $config->paths->$class . $url;
$url = $config->urls->$class . $url;
$mtime = filemtime($file);
if($config->debug) $version = filemtime($file);
}
$config->scripts->add($url . "?v=$mtime");
$config->scripts->add($url . "?v=$version");
} else {
$this->requested[$name] = $name;
}
@@ -185,4 +186,3 @@ abstract class ModuleJS extends WireData implements Module {
public function isSingular() { return true; }
public function isAutoload() { return false; }
}

View File

@@ -6,7 +6,7 @@
* Holds the place for a Module until it is included and instantiated.
* As used by the Modules class.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://.processwire.com
*
* @property bool $autoload
@@ -25,6 +25,7 @@ class ModulePlaceholder extends WireData implements Module {
protected $moduleInfo = array();
public function __construct() {
parent::__construct();
$this->set('autoload', false);
$this->set('singular', true);
$this->set('file', '');
@@ -35,7 +36,7 @@ class ModulePlaceholder extends WireData implements Module {
'title' => 'ModulePlaceholder: call $modules->get(class) to replace this placeholder.',
'version' => 0,
'summary' => '',
);
);
}
public function init() { }
@@ -43,11 +44,11 @@ class ModulePlaceholder extends WireData implements Module {
public function ___uninstall() { }
public function setClass($class) {
$this->class = $class;
$this->class = (string) $class;
}
public function setNamespace($ns) {
$this->ns = $ns;
$this->ns = (string) $ns;
}
public function get($key) {
@@ -65,10 +66,9 @@ class ModulePlaceholder extends WireData implements Module {
public function className($options = null) {
if($options === true || !empty($options['namespace'])) {
return trim($this->ns, '\\') . '\\' . $this->class;
return trim("$this->ns", '\\') . '\\' . $this->class;
}
return $this->class;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,91 @@
<?php namespace ProcessWire;
/**
* ProcessWire Modules: Class
*
* Base for Modules helper classes.
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
abstract class ModulesClass extends Wire {
/**
* @var Modules
*
*/
protected $modules;
/**
* Debug mode?
*
* @var bool
*
*/
protected $debug = false;
/**
* Construct
*
* @param Modules $modules
*/
public function __construct(Modules $modules) {
$this->modules = $modules;
$modules->wire($this);
parent::__construct();
}
/**
* Convert given value to module ID
*
* @param string|int|Module $name
* @return int Returns 0 if module not found
*
*/
protected function moduleID($name) {
return $this->modules->moduleID($name);
}
/**
* Convert given value to module name
*
* @param int|string|Module $id
* @return string Returns blank string if not found
*
*/
protected function moduleName($id) {
return $this->modules->moduleName($id);
}
/**
* Save to the modules log
*
* @param string $str Message to log
* @param array|string $options Specify module name (string) or options array
* @return WireLog
*
*/
public function log($str, $options = array()) {
return $this->modules->log($str, $options);
}
/**
* Record and log error message
*
* #pw-internal
*
* @param array|Wire|string $text
* @param int $flags
* @return Modules|WireArray
*
*/
public function error($text, $flags = 0) {
return $this->modules->error($text, $flags);
}
public function getDebugData() {
return array();
}
}

View File

@@ -0,0 +1,721 @@
<?php namespace ProcessWire;
/**
* ProcessWire Modules: Configs
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class ModulesConfigs extends ModulesClass {
/**
* Cached module configuration data indexed by module ID
*
* Values are integer 1 for modules that have config data but data is not yet loaded.
* Values are an array for modules have have config data and has been loaded.
*
*/
protected $configData = array();
/**
* Get or set module configuration data
*
* #pw-internal
*
* @param int $moduleID
* @param array $setData
* @return array|int|null Returns one of the following:
* - Array of module config data
* - Null if requested moduleID is not found
* - Integer 1 if config data is present but must be loaded from DB
*
*/
public function configData($moduleID, $setData = null) {
$moduleID = (int) $moduleID;
if($setData) {
$this->configData[$moduleID] = $setData;
return array();
} else if(isset($this->configData[$moduleID])) {
return $this->configData[$moduleID];
} else {
return null;
}
}
/**
* Return the URL where the module can be edited, configured or uninstalled
*
* If module is not installed, it returns URL to install the module.
*
* #pw-group-configuration
*
* @param string|Module $className
* @param bool $collapseInfo
* @return string
*
*/
public function getModuleEditUrl($className, $collapseInfo = true) {
if(!is_string($className)) $className = $this->modules->getModuleClass($className);
$url = $this->wire()->config->urls->admin . 'module/';
if(empty($className)) return $url;
if(!$this->modules->isInstalled($className)) return $this->modules->getModuleInstallUrl($className);
$url .= "edit/?name=$className";
if($collapseInfo) $url .= "&collapse_info=1";
return $url;
}
/**
* Given a module name, return an associative array of configuration data for it
*
* - Applicable only for modules that support configuration.
* - Configuration data is stored encoded in the database "modules" table "data" field.
*
* ~~~~~~
* // Getting, modifying and saving module config data
* $data = $modules->getConfig('HelloWorld');
* $data['greeting'] = 'Hello World! How are you today?';
* $modules->saveConfig('HelloWorld', $data);
*
* // Getting just one property 'apiKey' from module config data
* @apiKey = $modules->getConfig('HelloWorld', 'apiKey');
* ~~~~~~
*
* #pw-group-configuration
* #pw-changelog 3.0.16 Changed from more verbose name `getModuleConfigData()`, which can still be used.
*
* @param string|Module $class
* @param string $property Optionally just get value for a specific property (omit to get all config)
* @return array|string|int|float Module configuration data, returns array unless a specific $property was requested
* @see Modules::saveConfig()
* @since 3.0.16 Use method getModuleConfigData() with same arguments for prior versions (can also be used on any version).
*
*/
public function getConfig($class, $property = '') {
$emptyReturn = $property ? null : array();
$className = $class;
if(is_object($className)) $className = wireClassName($className->className(), false);
$id = $this->moduleID($className);
if(!$id) return $emptyReturn;
$data = isset($this->configData[$id]) ? $this->configData[$id] : null;
if($data === null) return $emptyReturn; // module has no config data
if(is_array($data)) {
// great
} else {
// configData===1 indicates data must be loaded from DB
$configable = $this->isConfigable($className);
if(!$configable) return $emptyReturn;
$database = $this->wire()->database;
$query = $database->prepare("SELECT data FROM modules WHERE id=:id", "modules.getConfig($className)"); // QA
$query->bindValue(":id", (int) $id, \PDO::PARAM_INT);
$query->execute();
$data = $query->fetchColumn();
$query->closeCursor();
if(strlen($data)) $data = wireDecodeJSON($data);
if(empty($data)) $data = array();
$this->configData[(int) $id] = $data;
}
if($property) return isset($data[$property]) ? $data[$property] : null;
return $data;
}
/**
* Is the given module interactively configurable?
*
* This method can be used to simply determine if a module is configurable (yes or no), or more specifically
* how it is configurable.
*
* ~~~~~
* // Determine IF a module is configurable
* if($modules->isConfigurable('HelloWorld')) {
* // Module is configurable
* } else {
* // Module is NOT configurable
* }
* ~~~~~
* ~~~~~
* // Determine HOW a module is configurable
* $configurable = $module->isConfigurable('HelloWorld');
* if($configurable === true) {
* // configurable in a way compatible with all past versions of ProcessWire
* } else if(is_string($configurable)) {
* // configurable via an external configuration file
* // file is identifed in $configurable variable
* } else if(is_int($configurable)) {
* // configurable via a method in the class
* // the $configurable variable contains a number with specifics
* } else {
* // module is NOT configurable
* }
* ~~~~~
*
* ### Return value details
*
* #### If module is configurable via external configuration file:
*
* - Returns string of full path/filename to `ModuleName.config.php` file
*
* #### If module is configurable because it implements a configurable module interface:
*
* - Returns boolean `true` if module is configurable via the static `getModuleConfigInputfields()` method.
* This particular method is compatible with all past versions of ProcessWire.
* - Returns integer `2` if module is configurable via the non-static `getModuleConfigInputfields()` and requires no arguments.
* - Returns integer `3` if module is configurable via the non-static `getModuleConfigInputfields()` and requires `$data` array.
* - Returns integer `4` if module is configurable via the non-static `getModuleConfigInputfields()` and requires `InputfieldWrapper` argument.
* - Returns integer `19` if module is configurable via non-static `getModuleConfigArray()` method.
* - Returns integer `20` if module is configurable via static `getModuleConfigArray()` method.
*
* #### If module is not configurable:
*
* - Returns boolean `false` if not configurable
*
* *This method is named isConfigurableModule() in ProcessWire versions prior to to 3.0.16.*
*
* #pw-group-configuration
*
* @param Module|string $class Module name
* @param bool $useCache Use caching? This accepts a few options:
* - Specify boolean `true` to allow use of cache when available (default behavior).
* - Specify boolean `false` to disable retrieval of this property from getModuleInfo (forces a new check).
* - Specify string `interface` to check only if module implements ConfigurableModule interface.
* - Specify string `file` to check only if module has a separate configuration class/file.
* @return bool|string|int See details about return values in method description.
* @since 3.0.16
*
* @todo this method has two distinct parts (file and interface) that need to be split in two methods.
*
*/
public function isConfigurable($class, $useCache = true) {
$className = $class;
$moduleInstance = null;
$namespace = $this->modules->info->getModuleNamespace($className);
if(is_object($className)) {
$moduleInstance = $className;
$className = $this->modules->getModuleClass($moduleInstance);
}
$nsClassName = $namespace . $className;
if($useCache === true || $useCache === 1 || $useCache === "1") {
$info = $this->modules->getModuleInfo($className);
// if regular module info doesn't have configurable info, attempt it from verbose module info
// should only be necessary for transition period between the 'configurable' property being
// moved from verbose to non-verbose module info (i.e. this line can be deleted after PW 2.7)
if($info['configurable'] === null) {
$info = $this->modules->getModuleInfoVerbose($className);
}
if(!$info['configurable']) {
if($moduleInstance instanceof ConfigurableModule) {
// re-try because moduleInfo may be temporarily incorrect for this request because of change in moduleInfo format
// this is due to reports of ProcessChangelogHooks not getting config data temporarily between 2.6.11 => 2.6.12
$this->error(
"Configurable module check failed for $className. " .
"If this error persists, please do a Modules > Refresh.",
Notice::debug
);
$useCache = false;
} else {
return false;
}
} else {
if($info['configurable'] === true) return $info['configurable'];
if($info['configurable'] === 1 || $info['configurable'] === "1") return true;
if(is_int($info['configurable']) || ctype_digit("$info[configurable]")) return (int) $info['configurable'];
if(strpos($info['configurable'], $className) === 0) {
if(empty($info['file'])) {
$info['file'] = $this->modules->files->getModuleFile($className);
}
if($info['file']) {
return dirname($info['file']) . "/$info[configurable]";
}
}
}
}
if($useCache !== "interface") {
// check for separate module configuration file
$dir = dirname($this->modules->files->getModuleFile($className));
if($dir) {
$files = array(
"$dir/{$className}Config.php",
"$dir/$className.config.php"
);
$found = false;
foreach($files as $file) {
if(!is_file($file)) continue;
$config = null; // include file may override
$this->modules->files->includeModuleFile($file, $className);
$classConfig = $nsClassName . 'Config';
if(class_exists($classConfig, false)) {
$parents = wireClassParents($classConfig, false);
if(is_array($parents) && in_array('ModuleConfig', $parents)) {
$found = $file;
break;
}
} else {
// bypass include_once, because we need to read $config every time
if(is_null($config)) {
$classInfo = $this->modules->files->getFileClassInfo($file);
if($classInfo['class']) {
// not safe to include because this is not just a file with a $config array
} else {
$ns = $this->modules->files->getFileNamespace($file);
$file = $this->modules->files->compile($className, $file, $ns);
if($file) {
/** @noinspection PhpIncludeInspection */
include($file);
}
}
}
if(!is_null($config)) {
// included file specified a $config array
$found = $file;
break;
}
}
}
if($found) return $found;
}
}
// if file-only check was requested and we reach this point, exit with false now
if($useCache === "file") return false;
// ConfigurableModule interface checks
$result = false;
foreach(array('getModuleConfigArray', 'getModuleConfigInputfields') as $method) {
$configurable = false;
// if we have a module instance, use that for our check
if($moduleInstance instanceof ConfigurableModule) {
if(method_exists($moduleInstance, $method)) {
$configurable = $method;
} else if(method_exists($moduleInstance, "___$method")) {
$configurable = "___$method";
}
}
// if we didn't have a module instance, load the file to find what we need to know
if(!$configurable) {
if(!wireClassExists($nsClassName, false)) {
$this->modules->includeModule($className);
}
$interfaces = wireClassImplements($nsClassName, false);
if(is_array($interfaces) && in_array('ConfigurableModule', $interfaces)) {
if(wireMethodExists($nsClassName, $method)) {
$configurable = $method;
} else if(wireMethodExists($nsClassName, "___$method")) {
$configurable = "___$method";
}
}
}
// if still not determined to be configurable, move on to next method
if(!$configurable) continue;
// now determine if static or non-static
$ref = new \ReflectionMethod(wireClassName($nsClassName, true), $configurable);
if($ref->isStatic()) {
// config method is implemented as a static method
if($method == 'getModuleConfigInputfields') {
// static getModuleConfigInputfields
$result = true;
} else {
// static getModuleConfigArray
$result = 20;
}
} else if($method == 'getModuleConfigInputfields') {
// non-static getModuleConfigInputfields
// we allow for different arguments, so determine what it needs
$parameters = $ref->getParameters();
if(count($parameters)) {
$param0 = reset($parameters);
if(strpos($param0, 'array') !== false || strpos($param0, '$data') !== false) {
// method requires a $data array (for compatibility with non-static version)
$result = 3;
} else if(strpos($param0, 'InputfieldWrapper') !== false || strpos($param0, 'inputfields') !== false) {
// method requires an empty InputfieldWrapper (as a convenience)
$result = 4;
}
}
// method requires no arguments
if(!$result) $result = 2;
} else {
// non-static getModuleConfigArray
$result = 19;
}
// if we make it here, we know we already have a result so can stop now
break;
}
return $result;
}
/**
* Indicates whether module accepts config settings, whether interactively or API only
*
* - Returns false if module does not accept config settings.
* - Returns integer `30` if module accepts config settings but is not interactively configurable.
* - Returns true, int or string if module is interactively configurable, see `Modules::isConfigurable()` return values.
*
* @param string|Module $class
* @param bool $useCache
* @return bool|int|string
* @since 3.0.179
*
*/
public function isConfigable($class, $useCache = true) {
if(is_object($class)) {
if($class instanceof ConfigModule) {
$result = 30;
} else {
$result = $this->isConfigurable($class, $useCache);
}
} else {
$result = $this->isConfigurable($class, $useCache);
if(!$result && wireInstanceOf($class, 'ConfigModule')) $result = 30;
}
return $result;
}
/**
* Populate configuration data to a ConfigurableModule
*
* If the Module has a 'setConfigData' method, it will send the array of data to that.
* Otherwise it will populate the properties individually.
*
* @param Module $module
* @param array|null $data Configuration data [key=value], or omit/null if you want it to retrieve the config data for you.
* @param array|null $extraData Additional runtime configuration data to merge (default=null) 3.0.169+
* @return bool True if configured, false if not configurable
*
*/
public function setModuleConfigData(Module $module, $data = null, $extraData = null) {
$configurable = $this->isConfigable($module);
if(!$configurable) return false;
if(!is_array($data)) $data = $this->getConfig($module);
if(is_array($extraData)) $data = array_merge($data, $extraData);
$nsClassName = $module->className(true);
$moduleName = $module->className(false);
if(is_string($configurable) && is_file($configurable) && strpos(basename($configurable), $moduleName) === 0) {
// get defaults from ModuleConfig class if available
$className = $nsClassName . 'Config';
$config = null; // may be overridden by included file
// $compile = strrpos($className, '\\') < 1 && $this->wire('config')->moduleCompile;
$configFile = '';
if(!class_exists($className, false)) {
$configFile = $this->modules->files->compile($className, $configurable);
// $configFile = $compile ? $this->wire('files')->compile($configurable) : $configurable;
if($configFile) {
/** @noinspection PhpIncludeInspection */
include_once($configFile);
}
}
if(wireClassExists($className)) {
$parents = wireClassParents($className, false);
if(is_array($parents) && in_array('ModuleConfig', $parents)) {
$moduleConfig = $this->wire(new $className());
if($moduleConfig instanceof ModuleConfig) {
$defaults = $moduleConfig->getDefaults();
$data = array_merge($defaults, $data);
}
}
} else {
// the file may have already been include_once before, so $config would not be set
// so we try a regular include() next.
if(is_null($config)) {
if(!$configFile) {
$configFile = $this->modules->files->compile($className, $configurable);
// $configFile = $compile ? $this->wire('files')->compile($configurable) : $configurable;
}
if($configFile) {
/** @noinspection PhpIncludeInspection */
include($configFile);
}
}
if(is_array($config)) {
// alternatively, file may just specify a $config array
/** @var ModuleConfig $moduleConfig */
$moduleConfig = $this->wire(new ModuleConfig());
$moduleConfig->add($config);
$defaults = $moduleConfig->getDefaults();
$data = array_merge($defaults, $data);
}
}
}
if(method_exists($module, 'setConfigData') || method_exists($module, '___setConfigData')) {
/** @var _Module $module */
$module->setConfigData($data);
return true;
}
foreach($data as $key => $value) {
$module->$key = $value;
}
return true;
}
/**
* Save provided configuration data for the given module
*
* - Applicable only for modules that support configuration.
* - Configuration data is stored encoded in the database "modules" table "data" field.
*
* ~~~~~~
* // Getting, modifying and saving module config data
* $data = $modules->getConfig('HelloWorld');
* $data['greeting'] = 'Hello World! How are you today?';
* $modules->saveConfig('HelloWorld', $data);
* ~~~~~~
*
* #pw-group-configuration
* #pw-group-manipulation
* #pw-changelog 3.0.16 Changed name from the more verbose saveModuleConfigData(), which will still work.
*
* @param string|Module $class Module or module name
* @param array|string $data Associative array of configuration data, or name of property you want to save.
* @param mixed|null $value If you specified a property in previous arg, the value for the property.
* @return bool True on success, false on failure
* @throws WireException
* @see Modules::getConfig()
* @since 3.0.16 Use method saveModuleConfigData() with same arguments for prior versions (can also be used on any version).
*
*/
public function saveConfig($class, $data, $value = null) {
$className = $class;
if(is_object($className)) $className = $className->className();
$moduleName = wireClassName($className, false);
$id = $this->moduleID($moduleName);
if(!$id) throw new WireException("Unable to find ID for Module '$moduleName'");
if(is_string($data)) {
// a property and value have been provided
$property = $data;
$data = $this->getConfig($class);
if(is_null($value)) {
// remove the property
unset($data[$property]);
} else {
// populate the value for the property
$data[$property] = $value;
}
} else {
// data must be an associative array of configuration data
if(!is_array($data)) return false;
}
// ensure original duplicates info is retained and validate that it is still current
$data = $this->modules->duplicates()->getDuplicatesConfigData($moduleName, $data);
$this->configData[$id] = $data;
$json = count($data) ? wireEncodeJSON($data, true) : '';
$database = $this->wire()->database;
$query = $database->prepare("UPDATE modules SET data=:data WHERE id=:id", "modules.saveConfig($moduleName)"); // QA
$query->bindValue(":data", $json, \PDO::PARAM_STR);
$query->bindValue(":id", (int) $id, \PDO::PARAM_INT);
$result = $query->execute();
// $this->log("Saved module '$moduleName' config data");
return $result;
}
/**
* Get the Inputfields that configure the given module or return null if not configurable
*
* #pw-internal
*
* @param string|Module|int $moduleName
* @param InputfieldWrapper|null $form Optionally specify the form you want Inputfields appended to.
* @return InputfieldWrapper|null
*
*/
public function getModuleConfigInputfields($moduleName, ?InputfieldWrapper $form = null) {
$moduleName = $this->modules->getModuleClass($moduleName);
$configurable = $this->isConfigurable($moduleName);
if(!$configurable) return null;
/** @var InputfieldWrapper $form */
if(is_null($form)) $form = $this->wire(new InputfieldWrapper());
$data = $this->getConfig($moduleName);
$fields = null;
// check for configurable module interface
$configurableInterface = $this->isConfigurable($moduleName, "interface");
if($configurableInterface) {
if(is_int($configurableInterface) && $configurableInterface > 1 && $configurableInterface < 20) {
// non-static
/** @var ConfigurableModule|Module|_Module $module */
if($configurableInterface === 2) {
// requires no arguments
$module = $this->modules->getModule($moduleName);
$fields = $module->getModuleConfigInputfields();
} else if($configurableInterface === 3) {
// requires $data array
$module = $this->modules->getModule($moduleName, array('noInit' => true, 'noCache' => true));
$this->setModuleConfigData($module);
$fields = $module->getModuleConfigInputfields($data);
} else if($configurableInterface === 4) {
// requires InputfieldWrapper
// we allow for option of no return statement in the method
$module = $this->modules->getModule($moduleName);
$fields = $this->wire(new InputfieldWrapper()); /** @var InputfieldWrapper $fields */
$fields->setParent($form);
$_fields = $module->getModuleConfigInputfields($fields);
if($_fields instanceof InputfieldWrapper) $fields = $_fields;
unset($_fields);
} else if($configurableInterface === 19) {
// non-static getModuleConfigArray method
$module = $this->modules->getModule($moduleName);
$fields = $this->wire(new InputfieldWrapper()); /** @var InputfieldWrapper $fields */
$fields->importArray($module->getModuleConfigArray());
$fields->populateValues($module);
}
} else if($configurableInterface === 20) {
// static getModuleConfigArray method
$fields = $this->wire(new InputfieldWrapper()); /** @var InputfieldWrapper $fields */
$fields->importArray(call_user_func(array(wireClassName($moduleName, true), 'getModuleConfigArray')));
$fields->populateValues($data);
} else {
// static getModuleConfigInputfields method
$nsClassName = $this->modules->info->getModuleNamespace($moduleName) . $moduleName;
$fields = call_user_func(array($nsClassName, 'getModuleConfigInputfields'), $data);
}
if($fields instanceof InputfieldWrapper) {
foreach($fields as $field) {
$form->append($field);
}
} else if($fields instanceof Inputfield) {
$form->append($fields);
} else {
$this->error("$moduleName.getModuleConfigInputfields() did not return InputfieldWrapper");
}
}
// check for file-based config
$file = $this->isConfigurable($moduleName, "file");
if(!$file || !is_string($file) || !is_file($file)) {
// config is not file-based
} else {
// file-based config
$config = null;
$ns = $this->modules->info->getModuleNamespace($moduleName);
$configClass = $ns . $moduleName . "Config";
if(!class_exists($configClass)) {
$configFile = $this->modules->files->compile($moduleName, $file, $ns);
if($configFile) {
/** @noinspection PhpIncludeInspection */
include_once($configFile);
}
}
$configModule = null;
if(wireClassExists($configClass)) {
// file contains a ModuleNameConfig class
$configModule = $this->wire(new $configClass());
} else {
if(is_null($config)) {
$configFile = $this->modules->files->compile($moduleName, $file, $ns);
if($configFile) {
/** @noinspection PhpIncludeInspection */
include($configFile); // in case of previous include_once
}
}
if(is_array($config)) {
// file contains a $config array
$configModule = $this->wire(new ModuleConfig());
$configModule->add($config);
}
}
if($configModule instanceof ModuleConfig) {
$defaults = $configModule->getDefaults();
$data = array_merge($defaults, $data);
$configModule->setArray($data);
$fields = $configModule->getInputfields();
if($fields instanceof InputfieldWrapper) {
foreach($fields as $field) {
$form->append($field);
}
foreach($data as $key => $value) {
$f = $form->getChildByName($key);
if(!$f) continue;
if($f instanceof InputfieldCheckbox && $value) {
$f->attr('checked', 'checked');
} else {
$f->attr('value', $value);
}
}
} else {
$this->error("$configModule.getInputfields() did not return InputfieldWrapper");
}
}
} // file-based config
if($form) {
// determine how many visible Inputfields there are in the module configuration
// for assignment or removal of flagsNoUserConfig flag when applicable
$numVisible = 0;
foreach($form->getAll() as $inputfield) {
if($inputfield instanceof InputfieldHidden || $inputfield instanceof InputfieldWrapper) continue;
$numVisible++;
}
$flags = $this->modules->flags->getFlags($moduleName);
if($numVisible) {
if($flags & Modules::flagsNoUserConfig) {
$info = $this->modules->info->getModuleInfoVerbose($moduleName);
if(empty($info['addFlag']) || !($info['addFlag'] & Modules::flagsNoUserConfig)) {
$this->modules->flags->setFlag($moduleName, Modules::flagsNoUserConfig, false); // remove flag
}
}
} else {
if(!($flags & Modules::flagsNoUserConfig)) {
if(empty($info['removeFlag']) || !($info['removeFlag'] & Modules::flagsNoUserConfig)) {
$this->modules->flags->setFlag($moduleName, Modules::flagsNoUserConfig, true); // add flag
}
}
}
}
return $form;
}
public function getDebugData() {
return array(
'configData' => $this->configData
);
}
}

View File

@@ -6,7 +6,7 @@
* Provides functions for managing sitautions where more than one
* copy of the same module is intalled. This is a helper for the Modules class.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
@@ -40,6 +40,7 @@ class ModulesDuplicates extends Wire {
*
*/
protected $numNewDuplicates = 0;
/**
* Return quantity of new duplicates found while loading modules
@@ -73,7 +74,7 @@ class ModulesDuplicates extends Wire {
public function hasDuplicate($className, $pathname = '') {
if(!isset($this->duplicates[$className])) return false;
if($pathname) {
$rootPath = $this->wire('config')->paths->root;
$rootPath = $this->wire()->config->paths->root;
if(strpos($pathname, $rootPath) === 0) $pathname = str_replace($rootPath, '/', $pathname);
return in_array($pathname, $this->duplicates[$className]);
}
@@ -90,7 +91,7 @@ class ModulesDuplicates extends Wire {
*/
public function addDuplicate($className, $pathname, $current = false) {
if(!isset($this->duplicates[$className])) $this->duplicates[$className] = array();
$rootPath = $this->wire('config')->paths->root;
$rootPath = $this->wire()->config->paths->root;
if(strpos($pathname, $rootPath) === 0) $pathname = str_replace($rootPath, '/', $pathname);
if(!in_array($pathname, $this->duplicates[$className])) {
$this->duplicates[$className][] = $pathname;
@@ -153,11 +154,12 @@ class ModulesDuplicates extends Wire {
public function getDuplicates($className = '') {
if(!$className) return $this->duplicates;
$className = $this->wire('modules')->getModuleClass($className);
$modules = $this->wire()->modules;
$className = $modules->getModuleClass($className);
$files = isset($this->duplicates[$className]) ? $this->duplicates[$className] : array();
$using = isset($this->duplicatesUse[$className]) ? $this->duplicatesUse[$className] : '';
$rootPath = $this->wire('config')->paths->root;
$rootPath = $this->wire()->config->paths->root;
foreach($files as $key => $file) {
$file = rtrim($rootPath, '/') . $file;
@@ -167,7 +169,7 @@ class ModulesDuplicates extends Wire {
}
if(count($files) > 1 && !$using) {
$using = $this->wire('modules')->getModuleFile($className);
$using = $modules->getModuleFile($className);
$using = str_replace($rootPath, '/', $using);
}
@@ -191,8 +193,9 @@ class ModulesDuplicates extends Wire {
*
*/
public function setUseDuplicate($className, $pathname) {
$className = $this->wire('modules')->getModuleClass($className);
$rootPath = $this->wire('config')->paths->root;
$modules = $this->wire()->modules;
$className = $modules->getModuleClass($className);
$rootPath = $this->wire()->config->paths->root;
if(!isset($this->duplicates[$className])) {
throw new WireException("Module $className does not have duplicates");
}
@@ -204,9 +207,9 @@ class ModulesDuplicates extends Wire {
throw new WireException("Duplicate module file does not exist: $pathname");
}
$this->duplicatesUse[$className] = $pathname;
$configData = $this->wire('modules')->getModuleConfigData($className);
$configData = $modules->getModuleConfigData($className);
$configData['-dups-use'] = $pathname;
$this->wire('modules')->saveModuleConfigData($className, $configData);
$modules->saveModuleConfigData($className, $configData);
}
/**
@@ -214,8 +217,9 @@ class ModulesDuplicates extends Wire {
*
*/
public function updateDuplicates() {
$rootPath = $this->wire('config')->paths->root;
$modules = $this->wire()->modules;
$rootPath = $this->wire()->config->paths->root;
// store duplicate information in each module's data field
foreach($this->getDuplicates() as $moduleName => $files) {
@@ -228,7 +232,7 @@ class ModulesDuplicates extends Wire {
$files[$key] = $file;
}
$files = array_unique($files);
$configData = $this->wire('modules')->getModuleConfigData($moduleName);
$configData = $modules->getModuleConfigData($moduleName);
if((empty($configData['-dups']) && !empty($files))
|| (empty($configData['-dups-use']) || $configData['-dups-use'] != $using)
|| (isset($configData['-dups']) && implode(' ', $configData['-dups']) != implode(' ', $files))
@@ -237,13 +241,13 @@ class ModulesDuplicates extends Wire {
$this->duplicatesUse[$moduleName] = $using;
$configData['-dups'] = $files;
$configData['-dups-use'] = $using;
$this->wire('modules')->saveModuleConfigData($moduleName, $configData);
$modules->saveModuleConfigData($moduleName, $configData);
}
}
// update any modules that no longer have duplicates
$removals = array();
$query = $this->wire('database')->prepare("SELECT `class`, `flags` FROM modules WHERE `flags` & :flag");
$query = $this->wire()->database->prepare("SELECT `class`, `flags` FROM modules WHERE `flags` & :flag");
$query->bindValue(':flag', Modules::flagsDuplicate, \PDO::PARAM_INT);
$query->execute();
@@ -258,10 +262,10 @@ class ModulesDuplicates extends Wire {
}
foreach($removals as $class => $flags) {
$this->wire('modules')->setFlags($class, $flags);
$configData = $this->wire('modules')->getModuleConfigData($class);
$modules->setFlags($class, $flags);
$configData = $modules->getModuleConfigData($class);
unset($configData['-dups'], $configData['-dups-use']);
$this->wire('modules')->saveModuleConfigData($class, $configData);
$modules->saveModuleConfigData($class, $configData);
}
}
@@ -275,7 +279,9 @@ class ModulesDuplicates extends Wire {
*
*/
public function recordDuplicate($basename, $pathname, $pathname2, &$installed) {
$rootPath = $this->wire('config')->paths->root;
$config = $this->wire()->config;
$modules = $this->wire()->modules;
$rootPath = $config->paths->root;
// ensure paths start from root of PW install
if(strpos($pathname, $rootPath) === 0) $pathname = str_replace($rootPath, '/', $pathname);
if(strpos($pathname2, $rootPath) === 0) $pathname2 = str_replace($rootPath, '/', $pathname2);
@@ -295,7 +301,7 @@ class ModulesDuplicates extends Wire {
if(isset($installed[$basename]['flags'])) {
$flags = $installed[$basename]['flags'];
} else {
$flags = $this->wire('modules')->getFlags($basename);
$flags = $modules->getFlags($basename);
}
if($flags & Modules::flagsDuplicate) {
// flags already represent duplicate status
@@ -303,14 +309,14 @@ class ModulesDuplicates extends Wire {
// make database aware this module has multiple files by adding the duplicate flag
$this->numNewDuplicates++; // trigger update needed
$flags = $flags | Modules::flagsDuplicate;
$this->wire('modules')->setFlags($basename, $flags);
$modules->setFlags($basename, $flags);
}
$err = sprintf($this->_('There appear to be multiple copies of module "%s" on the file system.'), $basename) . ' ';
$this->wire('log')->save('modules', $err);
$user = $this->wire('user');
$this->wire()->log->save('modules', $err);
$user = $this->wire()->user;
if($user && $user->isSuperuser()) {
$err .= $this->_('Please edit the module settings to tell ProcessWire which one to use:') . ' ' .
"<a href='" . $this->wire('config')->urls->admin . 'module/edit?name=' . $basename . "'>$basename</a>";
"<a href='" . $config->urls->admin . 'module/edit?name=' . $basename . "'>$basename</a>";
$this->warning($err, Notice::allowMarkup);
}
//$this->message("recordDuplicate($basename, $pathname) $this->numNewDuplicates"); //DEBUG
@@ -327,10 +333,11 @@ class ModulesDuplicates extends Wire {
*
*/
public function getDuplicatesConfigData($className, array $configData = array()) {
$config = $this->wire()->config;
// ensure original duplicates info is retained and validate that it is still current
if(isset($this->duplicates[$className])) {
foreach($this->duplicates[$className] as $key => $file) {
$pathname = rtrim($this->wire('config')->paths->root, '/') . $file;
$pathname = rtrim($config->paths->root, '/') . $file;
if(!file_exists($pathname)) {
unset($this->duplicates[$className][$key]);
}
@@ -341,7 +348,7 @@ class ModulesDuplicates extends Wire {
} else {
$configData['-dups'] = $this->duplicates[$className];
if(isset($this->duplicatesUse[$className])) {
$pathname = rtrim($this->wire('config')->paths->root, '/') . $this->duplicatesUse[$className];
$pathname = rtrim($config->paths->root, '/') . $this->duplicatesUse[$className];
if(file_exists($pathname)) {
$configData['-dups-use'] = $this->duplicatesUse[$className];
} else {
@@ -354,4 +361,11 @@ class ModulesDuplicates extends Wire {
}
return $configData;
}
public function getDebugData() {
return array(
'duplicates' => $this->duplicates,
'duplicatesUse' => $this->duplicatesUse
);
}
}

669
wire/core/ModulesFiles.php Normal file
View File

@@ -0,0 +1,669 @@
<?php namespace ProcessWire;
/**
* ProcessWire Modules: Files
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class ModulesFiles extends ModulesClass {
/**
* Core module types that are isolated by directory
*
* @var array
*
*/
protected $coreTypes = array(
'AdminTheme',
'Fieldtype',
'Inputfield',
'Jquery',
'LanguageSupport',
'Markup',
'Process',
'Session',
'System',
'Textformatter',
);
/**
* Module file extensions indexed by module name where value 1=.module, and 2=.module.php
*
* @var array
*
*/
protected $moduleFileExts = array();
/**
* Get or set module file extension type (1 or 2)
*
* @param string $class Module class name
* @param int $setValue 1 for '.module' or 2 for '.module.php', or omit to get current value
* @return int
*
*/
public function moduleFileExt($class, $setValue = null) {
if($setValue !== null) {
$this->moduleFileExts[$class] = (int) $setValue;
return $setValue;
}
return isset($this->moduleFileExts[$class]) ? $this->moduleFileExts[$class] : 0;
}
/**
* Find new module files in the given $path
*
* If $readCache is true, this will perform the find from the cache
*
* @param string $path Path to the modules
* @param bool $readCache Optional. If set to true, then this method will attempt to read modules from the cache.
* @param int $level For internal recursive use.
* @return array Array of module files
*
*/
public function findModuleFiles($path, $readCache = false, $level = 0) {
static $startPath;
static $prependFiles = array();
$config = $this->wire()->config;
$cacheName = '';
if($level == 0) {
$startPath = $path;
$cacheName = "Modules." . str_replace($config->paths->root, '', $path);
if($readCache) {
$cacheContents = $this->modules->getCache($cacheName);
if($cacheContents) return explode("\n", trim($cacheContents));
}
}
$files = array();
$autoloadOrders = $this->modules->loader->getAutoloadOrders();
if(count($autoloadOrders) && $path !== $config->paths->modules) {
// ok
} else {
$autoloadOrders = null;
}
try {
$dir = new \DirectoryIterator($path);
} catch(\Exception $e) {
$this->trackException($e, false, true);
$dir = null;
}
if($dir) foreach($dir as $file) {
if($file->isDot()) continue;
$filename = $file->getFilename();
$pathname = $file->getPathname();
if(DIRECTORY_SEPARATOR != '/') {
$pathname = str_replace(DIRECTORY_SEPARATOR, '/', $pathname);
}
if(strpos($pathname, '/.') !== false) {
$pos = strrpos(rtrim($pathname, '/'), '/');
if($pathname[$pos+1] == '.') continue; // skip hidden files and dirs
}
// if it's a directory with a .module file in it named the same as the dir, then descend into it
if($file->isDir() && ($level < 1 || (is_file("$pathname/$filename.module") || is_file("$pathname/$filename.module.php")))) {
$files = array_merge($files, $this->findModuleFiles($pathname, false, $level + 1));
}
// if the filename doesn't end with .module or .module.php, then stop and move onto the next
$extension = $file->getExtension();
if($extension !== 'module' && $extension !== 'php') continue;
list($moduleName, $extension) = explode('.', $filename, 2);
if($extension !== 'module' && $extension !== 'module.php') continue;
$pathname = str_replace($startPath, '', $pathname);
if($autoloadOrders !== null && isset($autoloadOrders[$moduleName])) {
$prependFiles[$pathname] = $autoloadOrders[$moduleName];
} else {
$files[] = $pathname;
}
}
if($level == 0 && $dir !== null) {
if(!empty($prependFiles)) {
// one or more non-core modules must be loaded first in a specific order
arsort($prependFiles);
$files = array_merge(array_keys($prependFiles), $files);
$prependFiles = array();
}
if($cacheName) {
$this->modules->saveCache($cacheName, implode("\n", $files));
}
}
return $files;
}
/**
* Get the path + filename (or optionally URL) for module
*
* @param string|Module $class Module class name or object instance
* @param array|bool $options Options to modify default behavior:
* - `getURL` (bool): Specify true if you want to get the URL rather than file path (default=false).
* - `fast` (bool): Specify true to omit file_exists() checks (default=false).
* - `guess` (bool): Manufacture/guess a module location if one cannot be found (default=false) 3.0.170+
* - Note: If you specify a boolean for the $options argument, it is assumed to be the $getURL property.
* @return bool|string Returns string of module file, or false on failure.
*
*/
public function getModuleFile($class, $options = array()) {
$config = $this->wire()->config;
$className = $class;
if(is_bool($options)) $options = array('getURL' => $options);
if(!isset($options['getURL'])) $options['getURL'] = false;
if(!isset($options['fast'])) $options['fast'] = false;
$file = false;
// first see it's an object, and if we can get the file from the object
if(is_object($className)) {
$module = $className;
if($module instanceof ModulePlaceholder) $file = $module->file;
$moduleName = $module->className();
$className = $module->className(true);
} else {
$moduleName = wireClassName($className, false);
}
$hasDuplicate = $this->modules->duplicates()->hasDuplicate($moduleName);
if(!$hasDuplicate) {
// see if we can determine it from already stored paths
$path = $config->paths($moduleName);
if($path) {
$file = $path . $moduleName . ($this->moduleFileExt($moduleName) === 2 ? '.module.php' : '.module');
if(!$options['fast'] && !file_exists($file)) $file = false;
}
}
// next see if we've already got the module filename cached locally
if(!$file) {
$installableFile = $this->modules->installableFile($moduleName);
if($installableFile && !$hasDuplicate) {
$file = $installableFile;
if(!$options['fast'] && !file_exists($file)) $file = false;
}
}
if(!$file) {
$dupFile = $this->modules->duplicates()->getCurrent($moduleName);
if($dupFile) {
$rootPath = $config->paths->root;
$file = rtrim($rootPath, '/') . $dupFile;
if(!file_exists($file)) {
// module in use may have been deleted, find the next available one that exists
$file = '';
$dups = $this->modules->duplicates()->getDuplicates($moduleName);
foreach($dups['files'] as $pathname) {
$pathname = rtrim($rootPath, '/') . $pathname;
if(file_exists($pathname)) $file = $pathname;
if($file) break;
}
}
}
}
if(!$file) {
// see if it's a predefined core type that can be determined from the type
// this should only come into play if module has moved or had a load error
foreach($this->coreTypes as $typeName) {
if(strpos($moduleName, $typeName) !== 0) continue;
$checkFiles = array(
"$typeName/$moduleName/$moduleName.module",
"$typeName/$moduleName/$moduleName.module.php",
"$typeName/$moduleName.module",
"$typeName/$moduleName.module.php",
);
$path1 = $config->paths->modules;
foreach($checkFiles as $checkFile) {
$file1 = $path1 . $checkFile;
if(file_exists($file1)) $file = $file1;
if($file) break;
}
if($file) break;
}
if(!$file) {
// check site modules
$checkFiles = array(
"$moduleName/$moduleName.module",
"$moduleName/$moduleName.module.php",
"$moduleName.module",
"$moduleName.module.php",
);
$path1 = $config->paths->siteModules;
foreach($checkFiles as $checkFile) {
$file1 = $path1 . $checkFile;
if(file_exists($file1)) $file = $file1;
if($file) break;
}
}
}
if(!$file) {
// if all the above failed, try to get it from Reflection
try {
// note we don't call getModuleClass() here because it may result in a circular reference
if(strpos($className, "\\") === false) {
$moduleID = $this->moduleID($moduleName);
$namespace = $this->modules->info->moduleInfoCache($moduleID, 'namespace');
if(!empty($namespace)) {
$className = rtrim($namespace, "\\") . "\\$moduleName";
} else {
$className = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\$moduleName" : $moduleName;
}
}
$reflector = new \ReflectionClass($className);
$file = $reflector->getFileName();
} catch(\Exception $e) {
$file = false;
}
}
if(!$file && !empty($options['guess'])) {
// make a guess about where module would be if we had been able to find it
$file = $config->paths('siteModules') . "$moduleName/$moduleName.module";
}
if($file) {
if(DIRECTORY_SEPARATOR != '/') $file = str_replace(DIRECTORY_SEPARATOR, '/', $file);
if($options['getURL']) $file = str_replace($config->paths->root, '/', $file);
}
return $file;
}
/**
* Include the given filename
*
* @param string $file
* @param string $moduleName
* @return bool
*
*/
public function includeModuleFile($file, $moduleName) {
$wire1 = ProcessWire::getCurrentInstance();
$wire2 = $this->wire();
// check if there is more than one PW instance active
if($wire1 !== $wire2) {
// multi-instance is active, don't autoload module if class already exists
// first do a fast check, which should catch any core modules
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
// temporarily set the $wire instance to 2nd instance during include()
ProcessWire::setCurrentInstance($wire2);
}
// get compiled version (if it needs compilation)
$file = $this->compile($moduleName, $file);
if($file) {
/** @noinspection PhpIncludeInspection */
$success = @include_once($file);
} else {
$success = false;
}
if(!$success) {
// handle case where module has moved from /modules/Foo.module to /modules/Foo/Foo.module
// which can only occur during upgrades from much older versions.
// examples are FieldtypeImage and FieldtypeText which moved to their own directories.
$file2 = preg_replace('!([/\\\\])([^/\\\\]+)(\.module(?:\.php)?)$!', '$1$2$1$2$3', $file);
if($file !== $file2) $success = @include_once($file2);
}
// set instance back, if multi-instance
if($wire1 !== $wire2) ProcessWire::setCurrentInstance($wire1);
return (bool) $success;
}
/**
* Compile and return the given file for module, if allowed to do so
*
* @param Module|string $moduleName
* @param string $file Optionally specify the module filename as an optimization
* @param string|null $namespace Optionally specify namespace as an optimization
* @return string|bool
*
*/
public function compile($moduleName, $file = '', $namespace = null) {
static $allowCompile = null;
if($allowCompile === null) $allowCompile = $this->wire()->config->moduleCompile;
// if not given a file, track it down
if(empty($file)) $file = $this->modules->getModuleFile($moduleName);
// don't compile when module compilation is disabled
if(!$allowCompile) return $file;
// don't compile core modules
if(strpos($file, $this->modules->coreModulesDir) !== false) return $file;
// if namespace not provided, get it
if(is_null($namespace)) {
if(is_object($moduleName)) {
$className = $moduleName->className(true);
$namespace = wireClassName($className, 1);
} else if(is_string($moduleName) && strpos($moduleName, "\\") !== false) {
$namespace = wireClassName($moduleName, 1);
} else {
$namespace = $this->modules->info->getModuleNamespace($moduleName, array('file' => $file));
}
}
// determine if compiler should be used
if(__NAMESPACE__) {
$compile = $namespace === '\\' || empty($namespace);
} else {
$compile = trim($namespace, '\\') === 'ProcessWire';
}
// compile if necessary
if($compile) {
/** @var FileCompiler $compiler */
$compiler = $this->wire(new FileCompiler(dirname($file)));
$compiledFile = $compiler->compile(basename($file));
if($compiledFile) $file = $compiledFile;
}
return $file;
}
/**
* Find modules that are missing their module file on the file system
*
* Return value is array:
* ~~~~~
* [
* 'ModuleName' => [
* 'id' => 123,
* 'name' => 'ModuleName',
* 'file' => '/path/to/expected/file.module'
* ],
* 'ModuleName' => [
* ...
* ]
* ];
* ~~~~~
*
* #pw-internal
*
* @return array
* @since 3.0.170
*
*/
public function findMissingModules() {
$missing = array();
$unflags = array();
$sql = "SELECT id, class FROM modules WHERE flags & :flagsNoFile ORDER BY class";
$query = $this->wire()->database->prepare($sql);
$query->bindValue(':flagsNoFile', Modules::flagsNoFile, \PDO::PARAM_INT);
$query->execute();
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
$class = $row['class'];
if(strpos($class, '.') === 0) continue;
$file = $this->getModuleFile($class, array('fast' => true));
if($file && file_exists($file)) {
$unflags[] = $class;
continue;
}
$fileAlt = $this->getModuleFile($class, array('fast' => false));
if($fileAlt) {
$file = $fileAlt;
if(file_exists($file)) continue;
}
if(!$file) {
$file = $this->getModuleFile($class, array('fast' => true, 'guess' => true));
}
$missing[$class] = array(
'id' => $row['id'],
'name' => $class,
'file' => $file,
);
}
foreach($unflags as $name) {
$this->modules->flags->setFlag($name, Modules::flagsNoFile, false);
}
return $missing;
}
/**
* Load module related CSS and JS files (where applicable)
*
* - Applies only to modules that carry class-named CSS and/or JS files, such as Process, Inputfield and ModuleJS modules.
* - Assets are populated to `$config->styles` and `$config->scripts`.
*
* #pw-internal
*
* @param Module|int|string $module Module object or class name
* @return int Returns number of files that were added
*
*/
public function loadModuleFileAssets($module) {
$class = $this->modules->getModuleClass($module);
static $classes = array();
if(isset($classes[$class])) return 0; // already loaded
$config = $this->wire()->config;
$path = $config->paths($class);
$url = $config->urls($class);
$debug = $config->debug;
$coreVersion = $config->version;
$moduleVersion = 0;
$cnt = 0;
foreach(array('styles' => 'css', 'scripts' => 'js') as $type => $ext) {
$fileURL = '';
$file = "$path$class.$ext";
$fileVersion = $coreVersion;
$minFile = "$path$class.min.$ext";
if(!$debug && is_file($minFile)) {
$fileURL = "$url$class.min.$ext";
} else if(is_file($file)) {
$fileURL = "$url$class.$ext";
if($debug) $fileVersion = filemtime($file);
}
if($fileURL) {
if(!$moduleVersion) {
$info = $this->modules->info->getModuleInfo($module, array('verbose' => false));
$moduleVersion = (int) isset($info['version']) ? $info['version'] : 0;
}
$config->$type->add("$fileURL?v=$moduleVersion-$fileVersion");
$cnt++;
}
}
$classes[$class] = true;
return $cnt;
}
/**
* Get module language translation files
*
* @param Module|string $module
* @return array Array of translation files including full path, indexed by basename without extension
* @since 3.0.181
*
*/
public function getModuleLanguageFiles($module) {
$module = $this->modules->getModuleClass($module);
if(empty($module)) return array();
$path = $this->wire()->config->paths($module);
if(empty($path)) return array();
$pathHidden = $path . '.languages/';
$pathVisible = $path . 'languages/';
if(is_dir($pathVisible)) {
$path = $pathVisible;
} else if(is_dir($pathHidden)) {
$path = $pathHidden;
} else {
return array();
}
$items = array();
$options = array(
'extensions' => array('csv'),
'recursive' => false,
'excludeHidden' => true,
);
foreach($this->wire()->files->find($path, $options) as $file) {
$basename = basename($file, '.csv');
$items[$basename] = $file;
}
return $items;
}
/**
* Setup entries in config->urls and config->paths for the given module
*
* @param string $moduleName
* @param string $path
*
*/
public function setConfigPaths($moduleName, $path) {
$config = $this->wire()->config;
$rootPath = $config->paths->root;
if(strpos($path, $rootPath) === 0) {
// if root path included, strip it out
$path = substr($path, strlen($config->paths->root));
}
$path = rtrim($path, '/') . '/';
$config->paths->set($moduleName, $path);
$config->urls->set($moduleName, $path);
}
/**
* Get the namespace used in the given .php or .module file
*
* #pw-internal
*
* @param string $file
* @return string Includes leading and trailing backslashes where applicable
*
*/
public function getFileNamespace($file) {
$namespace = $this->wire()->files->getNamespace($file);
if($namespace !== "\\") $namespace = "\\" . trim($namespace, "\\") . "\\";
return $namespace;
}
/**
* Get the class defined in the file (or optionally the 'extends' or 'implements')
*
* #pw-internal
*
* @param string $file
* @return array Returns array with these indexes:
* 'class' => string (class without namespace)
* 'className' => string (class with namespace)
* 'extends' => string
* 'namespace' => string
* 'implements' => array
*
*/
public function getFileClassInfo($file) {
$value = array(
'class' => '',
'className' => '',
'extends' => '',
'namespace' => '',
'implements' => array()
);
if(!is_file($file)) return $value;
$data = file_get_contents($file);
if(!strpos($data, 'class')) return $value;
if(!preg_match('/^\s*class\s+(.+)$/m', $data, $matches)) return $value;
if(strpos($matches[1], "\t") !== false) $matches[1] = str_replace("\t", " ", $matches[1]);
$parts = explode(' ', trim($matches[1]));
foreach($parts as $key => $part) {
if(empty($part)) unset($parts[$key]);
}
$className = array_shift($parts);
if(strpos($className, '\\') !== false) {
$className = trim($className, '\\');
$a = explode('\\', $className);
$value['className'] = "\\$className\\";
$value['class'] = array_pop($a);
$value['namespace'] = '\\' . implode('\\', $a) . '\\';
} else {
$value['className'] = '\\' . $className;
$value['class'] = $className;
$value['namespace'] = '\\';
}
while(count($parts)) {
$next = array_shift($parts);
if($next == 'extends') {
$value['extends'] = array_shift($parts);
} else if($next == 'implements') {
$implements = array_shift($parts);
if(strlen($implements)) {
$implements = str_replace(' ', '', $implements);
$value['implements'] = explode(',', $implements);
}
}
}
return $value;
}
public function getDebugData() {
return array(
'moduleFileExts' => $this->moduleFileExts
);
}
}

200
wire/core/ModulesFlags.php Normal file
View File

@@ -0,0 +1,200 @@
<?php namespace ProcessWire;
/**
* ProcessWire Modules: Flags
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class ModulesFlags extends ModulesClass {
/**
* Array of module ID => flags (int)
*
* @var array
*
*/
protected $moduleFlags = array();
/**
* Get or set flags for module by module ID
*
* Omit all arguments to get flags for all modules indexed by module ID.
*
* Returns null if given module ID not found.
*
* @param int $moduleID This method only accepts module ID
* @param int $setValue Flag(s) to set
* @return array|mixed|null
*
*/
public function moduleFlags($moduleID = null, $setValue = null) {
if($moduleID === null) return $this->moduleFlags;
if(!ctype_digit("$moduleID")) $moduleID = $this->moduleID($moduleID);
if($setValue !== null) {
$this->moduleFlags[(int) $moduleID] = (int) $setValue;
} else if(isset($this->moduleFlags[$moduleID])) {
return $this->moduleFlags[$moduleID];
}
return null;
}
/**
* Get flags for the given module
*
* @param int|string|Module $id Module to add flag to
* @return int|false Returns integer flags on success, or boolean false on fail
*
*/
public function getFlags($id) {
$id = ctype_digit("$id") ? (int) $id : $this->modules->getModuleID($id);
if(isset($this->moduleFlags[$id])) return $this->moduleFlags[$id];
if(!$id) return false;
$query = $this->wire()->database->prepare('SELECT flags FROM modules WHERE id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
if(!$query->rowCount()) return false;
list($flags) = $query->fetch(\PDO::FETCH_NUM);
$flags = (int) $flags;
$this->moduleFlags[$id] = $flags;
return $flags;
}
/**
* Does module have flag?
*
* #pw-internal
*
* @param int|string|Module $id Module ID, class name or instance
* @param int $flag
* @return bool
* @since 3.0.170
*
*/
public function hasFlag($id, $flag) {
$flags = $this->getFlags($id);
return $flags === false ? false : ($flags & $flag);
}
/**
* Set module flags
*
* #pw-internal
*
* @param string|int $id Module id or class
* @param $flags
* @return bool
*
*/
public function setFlags($id, $flags) {
$flags = (int) $flags;
$id = ctype_digit("$id") ? (int) $id : $this->modules->getModuleID($id);
if(!$id) return false;
if($this->moduleFlags[$id] === $flags) return true;
$query = $this->wire()->database->prepare('UPDATE modules SET flags=:flags WHERE id=:id');
$query->bindValue(':flags', $flags);
$query->bindValue(':id', $id);
if($this->debug) $this->message("setFlags(" . $this->modules->getModuleClass($id) . ", " . $this->moduleFlags[$id] . " => $flags)");
$this->moduleFlags[$id] = $flags;
return $query->execute();
}
/**
* Add or remove a flag from a module
*
* #pw-internal
*
* @param $id int|string|Module $class Module to add flag to
* @param $flag int Flag to add (see flags* constants)
* @param $add bool $add Specify true to add the flag or false to remove it
* @return bool True on success, false on fail
*
*/
public function setFlag($id, $flag, $add = true) {
$id = ctype_digit("$id") ? (int) $id : $this->modules->getModuleID($id);
if(!$id) return false;
$flag = (int) $flag;
if(!$flag) return false;
$flags = $this->getFlags($id);
if($add) {
if($flags & $flag) return true; // already has the flag
$flags = $flags | $flag;
} else {
if(!($flags & $flag)) return true; // doesn't already have the flag
$flags = $flags & ~$flag;
}
$this->setFlags($id, $flags);
return true;
}
/**
* Update module flags if any happen to differ from what's in the given moduleInfo
*
* @param int $moduleID
* @param array $info
*
*/
public function updateModuleFlags($moduleID, array $info) {
$flags = (int) $this->getFlags($moduleID);
if($info['autoload']) {
// module is autoload
if(!($flags & Modules::flagsAutoload)) {
// add autoload flag
$this->setFlag($moduleID, Modules::flagsAutoload, true);
}
if(is_string($info['autoload'])) {
// requires conditional flag
// value is either: "function", or the conditional string (like key=value)
if(!($flags & Modules::flagsConditional)) $this->setFlag($moduleID, Modules::flagsConditional, true);
} else {
// should not have conditional flag
if($flags & Modules::flagsConditional) $this->setFlag($moduleID, Modules::flagsConditional, false);
}
} else if($info['autoload'] !== null) {
// module is not autoload
if($flags & Modules::flagsAutoload) {
// remove autoload flag
$this->setFlag($moduleID, Modules::flagsAutoload, false);
}
if($flags & Modules::flagsConditional) {
// remove conditional flag
$this->setFlag($moduleID, Modules::flagsConditional, false);
}
}
if($info['singular']) {
if(!($flags & Modules::flagsSingular)) $this->setFlag($moduleID, Modules::flagsSingular, true);
} else {
if($flags & Modules::flagsSingular) $this->setFlag($moduleID, Modules::flagsSingular, false);
}
// handle addFlag and removeFlag moduleInfo properties
foreach(array(0 => 'removeFlag', 1 => 'addFlag') as $add => $flagsType) {
if(empty($info[$flagsType])) continue;
if($flags & $info[$flagsType]) {
// already has the flags
if(!$add) {
// remove the flag(s)
$this->setFlag($moduleID, $info[$flagsType], false);
}
} else {
// does not have the flags
if($add) {
// add the flag(s)
$this->setFlag($moduleID, $info[$flagsType], true);
}
}
}
}
public function getDebugData() {
return array(
'moduleFlags' => $this->moduleFlags
);
}
}

1393
wire/core/ModulesInfo.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,843 @@
<?php namespace ProcessWire;
/**
* ProcessWire Modules: Installer
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class ModulesInstaller extends ModulesClass {
/**
* Get an associative array [name => path] for all modules that arent currently installed.
*
* #pw-internal
*
* @return array Array of elements with $moduleName => $pathName
*
*/
public function getInstallable() {
return $this->modules->getInstallable();
}
/**
* Is the given module name installable? (i.e. not already installed)
*
* #pw-internal
*
* @param string $class Module class name
* @param bool $now Is module installable RIGHT NOW? This makes it check that all dependencies are already fulfilled (default=false)
* @return bool True if module is installable, false if not
*
*/
public function isInstallable($class, $now = false) {
$installableFiles = $this->modules->installableFiles;
if(!array_key_exists($class, $installableFiles)) return false;
if(!wireInstanceOf($class, 'Module')) {
$nsClass = $this->modules->getModuleClass($class, true);
if(!wireInstanceOf($nsClass, 'ProcessWire\\Module')) return false;
}
if($now) {
$requires = $this->getRequiresForInstall($class);
if(count($requires)) return false;
}
return true;
}
/**
* Install the given module name
*
* #pw-group-manipulation
*
* @param string $class Module name (class name)
* @param array|bool $options Optional associative array that can contain any of the following:
* - `dependencies` (boolean): When true, dependencies will also be installed where possible. Specify false to prevent installation of uninstalled modules. (default=true)
* - `resetCache` (boolean): When true, module caches will be reset after installation. (default=true)
* - `force` (boolean): Force installation, even if dependencies can't be met.
* @return null|Module Returns null if unable to install, or ready-to-use Module object if successfully installed.
* @throws WireException
*
*/
public function install($class, $options = array()) {
$defaults = array(
'dependencies' => true,
'resetCache' => true,
'force' => false,
);
if(is_bool($options)) {
// dependencies argument allowed instead of $options, for backwards compatibility
$dependencies = $options;
$options = array('dependencies' => $dependencies);
}
$options = array_merge($defaults, $options);
$dependencyOptions = $options;
$dependencyOptions['resetCache'] = false;
if(!$this->isInstallable($class)) return null;
$requires = $this->getRequiresForInstall($class);
if(count($requires)) {
$error = '';
$installable = false;
if($options['dependencies']) {
$installable = true;
foreach($requires as $requiresModule) {
if(!$this->isInstallable($requiresModule)) $installable = false;
}
if($installable) {
foreach($requires as $requiresModule) {
if(!$this->modules->install($requiresModule, $dependencyOptions)) {
$error = $this->_('Unable to install required module') . " - $requiresModule. ";
$installable = false;
break;
}
}
}
}
if(!$installable) {
$error = sprintf($this->_('Module %s requires: %s'), $class, implode(', ', $requires)) . ' ' . $error;
if($options['force']) {
$this->warning($this->_('Warning!') . ' ' . $error);
} else {
throw new WireException($error);
}
}
}
$database = $this->wire()->database;
$languages = $this->wire()->languages;
$config = $this->wire()->config;
if($languages) $languages->setDefault();
$pathname = $this->modules->installableFile($class);
if(strpos($class, "\\") === false) {
$ns = $this->modules->info->getModuleNamespace($class, array(
'file' => $pathname
));
$nsClass = $ns . $class;
} else {
$nsClass = $class;
}
if(!class_exists($nsClass, false)) {
$this->modules->files->includeModuleFile($pathname, $class);
$this->modules->files->setConfigPaths($class, dirname($pathname));
}
$module = $this->modules->newModule($nsClass, $class);
if(!$module) return null;
$flags = 0;
$moduleID = 0;
if($this->modules->isSingular($module)) $flags = $flags | Modules::flagsSingular;
if($this->modules->isAutoload($module)) $flags = $flags | Modules::flagsAutoload;
$sql = "INSERT INTO modules SET class=:class, flags=:flags, data=''";
if($config->systemVersion >= 7) $sql .= ", created=NOW()";
$query = $database->prepare($sql, "modules.install($class)");
$query->bindValue(":class", $class, \PDO::PARAM_STR);
$query->bindValue(":flags", $flags, \PDO::PARAM_INT);
try {
if($query->execute()) $moduleID = (int) $database->lastInsertId();
} catch(\Exception $e) {
if($languages) $languages->unsetDefault();
$this->trackException($e, false, true);
return null;
}
$this->modules->moduleID($class, $moduleID);
$this->modules->add($module);
$this->modules->installableFile($class, false); // unset
// note: the module's install is called here because it may need to know its module ID for installation of permissions, etc.
if(method_exists($module, '___install') || method_exists($module, 'install')) {
try {
/** @var _Module $module */
$module->install();
} catch(\PDOException $e) {
$error = $this->_('Module reported error during install') . " ($class): " . $e->getMessage();
$this->error($error);
$this->trackException($e, false, $error);
} catch(\Exception $e) {
// remove the module from the modules table if the install failed
$moduleID = (int) $moduleID;
$error = $this->_('Unable to install module') . " ($class): " . $e->getMessage();
$ee = null;
try {
$query = $database->prepare('DELETE FROM modules WHERE id=:id LIMIT 1'); // QA
$query->bindValue(":id", $moduleID, \PDO::PARAM_INT);
$query->execute();
} catch(\Exception $ee) {
$this->trackException($e, false, $error)->trackException($ee, true);
}
if($languages) $languages->unsetDefault();
if(is_null($ee)) $this->trackException($e, false, $error);
return null;
}
}
$info = $this->modules->info->getModuleInfoVerbose($class, array('noCache' => true));
$sanitizer = $this->wire()->sanitizer;
$permissions = $this->wire()->permissions;
// if this module has custom permissions defined in its getModuleInfo()['permissions'] array, install them
foreach($info['permissions'] as $name => $title) {
$name = $sanitizer->pageName($name);
if(ctype_digit("$name") || empty($name)) continue; // permission name not valid
$permission = $permissions->get($name);
if($permission->id) continue; // permision already there
try {
$permission = $permissions->add($name);
$permission->title = $title;
$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);
}
}
// check if there are any modules in 'installs' that this module didn't handle installation of, and install them
$label = $this->_('Module Auto Install');
foreach($info['installs'] as $name) {
if(!$this->modules->isInstalled($name)) {
try {
$this->modules->install($name, $dependencyOptions);
$this->message("$label: $name");
} catch(\Exception $e) {
$error = "$label: $name - " . $e->getMessage();
$this->trackException($e, false, $error);
}
}
}
$this->log("Installed module '$module'");
if($languages) $languages->unsetDefault();
if($options['resetCache']) $this->modules->info->clearModuleInfoCache();
return $module;
}
/**
* Returns whether the module can be uninstalled
*
* #pw-internal
*
* @param string|Module $class
* @param bool $returnReason If true, the reason why it can't be uninstalled with be returned rather than boolean false.
* @return bool|string
*
*/
public function isUninstallable($class, $returnReason = false) {
$reason = '';
$reason1 = $this->_("Module is not already installed");
$namespace = $this->modules->info->getModuleNamespace($class);
$class = $this->modules->getModuleClass($class);
if(!$this->modules->isInstalled($class)) {
$reason = $reason1 . ' (a)';
} else {
$this->modules->includeModule($class);
if(!wireClassExists($namespace . $class, false)) {
$reason = $reason1 . " (b: $namespace$class)";
}
}
if(!$reason) {
// if the moduleInfo contains a non-empty 'permanent' property, then it's not uninstallable
$info = $this->modules->info->getModuleInfo($class);
if(!empty($info['permanent'])) {
$reason = $this->_("Module is permanent");
} else {
$dependents = $this->getRequiresForUninstall($class);
if(count($dependents)) $reason = $this->_("Module is required by other modules that must be removed first");
}
if(!$reason && in_array('Fieldtype', wireClassParents($namespace . $class))) {
foreach($this->wire()->fields as $field) {
$fieldtype = wireClassName($field->type, false);
if($fieldtype == $class) {
$reason = $this->_("This module is a Fieldtype currently in use by one or more fields");
break;
}
}
}
}
if($returnReason && $reason) return $reason;
return $reason ? false : true;
}
/**
* Returns whether the module can be deleted (have it's files physically removed)
*
* #pw-internal
*
* @param string|Module $class
* @param bool $returnReason If true, the reason why it can't be removed will be returned rather than boolean false.
* @return bool|string
*
*/
public function isDeleteable($class, $returnReason = false) {
$reason = '';
$class = $this->modules->getModuleClass($class);
$filename = $this->modules->installableFile($class);
$dirname = dirname($filename);
if(empty($filename) || $this->modules->isInstalled($class)) {
$reason = "Module must be uninstalled before it can be deleted.";
} else if(is_link($filename) || is_link($dirname) || is_link(dirname($dirname))) {
$reason = "Module is linked to another location";
} else if(!is_file($filename)) {
$reason = "Module file does not exist";
} else if(strpos($filename, $this->modules->coreModulesPath) === 0) {
$reason = "Core modules may not be deleted.";
} else if(!is_writable($filename)) {
$reason = "We have no write access to the module file, it must be removed manually.";
}
if($returnReason && $reason) return $reason;
return $reason ? false : true;
}
/**
* Delete the given module, physically removing its files
*
* #pw-group-manipulation
*
* @param string $class Module name (class name)
* @return bool
* @throws WireException If module can't be deleted, exception will be thrown containing reason.
*
*/
public function delete($class) {
$config = $this->wire()->config;
$fileTools = $this->wire()->files;
$class = $this->modules->getModuleClass($class);
$success = true;
$reason = $this->isDeleteable($class, true);
if($reason !== true) throw new WireException($reason);
$siteModulesPath = $config->paths->siteModules;
$filename = $this->modules->installableFile($class);
$basename = basename($filename);
// double check that $class is consistent with the actual $basename
if($basename === "$class.module" || $basename === "$class.module.php") {
// good, this is consistent with the format we require
} else {
throw new WireException("Unrecognized module filename format");
}
// now determine if module is the owner of the directory it exists in
// this is the case if the module class name is the same as the directory name
$path = dirname($filename); // full path to directory, i.e. .../site/modules/ProcessHello
$name = basename($path); // just name of directory that module is, i.e. ProcessHello
$parentPath = dirname($path); // full path to parent directory, i.e. ../site/modules
$backupPath = $parentPath . "/.$name"; // backup path, in case module is backed up
// first check that we are still in the /site/modules/ (or another non core modules path)
$inPath = false; // is module somewhere beneath /site/modules/ ?
$inRoot = false; // is module in /site/modules/ root? i.e. /site/modules/ModuleName.module
foreach($this->modules->getPaths() as $key => $modulesPath) {
if($key === 0) continue; // skip core modules path
if(strpos("$parentPath/", $modulesPath) === 0) $inPath = true;
if($modulesPath === $path) $inRoot = true;
}
$basename = basename($basename, '.php');
$basename = basename($basename, '.module');
$files = array(
"$basename.module",
"$basename.module.php",
"$basename.info.php",
"$basename.info.json",
"$basename.config.php",
"{$basename}Config.php",
);
if($inPath) {
// module is in /site/modules/[ModuleName]/
$numOtherModules = 0; // num modules in dir other than this one
$numLinks = 0; // number of symbolic links
$dirs = array("$path/");
do {
$dir = array_shift($dirs);
$this->message("Scanning: $dir", Notice::debug);
foreach(new \DirectoryIterator($dir) as $file) {
if($file->isDot()) continue;
if($file->isLink()) {
$numLinks++;
continue;
}
if($file->isDir()) {
$dirs[] = $fileTools->unixDirName($file->getPathname());
continue;
}
if(in_array($file->getBasename(), $files)) continue; // skip known files
if(strpos($file->getBasename(), '.module') && preg_match('{(\.module|\.module\.php)$}', $file->getBasename())) {
// another module exists in this dir, so we don't want to delete that
$numOtherModules++;
}
if(preg_match('{^(' . $basename . '\.[-_.a-zA-Z0-9]+)$}', $file->getBasename(), $matches)) {
// keep track of potentially related files in case we have to delete them individually
$files[] = $matches[1];
}
}
} while(count($dirs));
if(!$inRoot && !$numOtherModules && !$numLinks) {
// the modulePath had no other modules or directories in it, so we can delete it entirely
$success = (bool) $fileTools->rmdir($path, true);
if($success) {
$this->message("Removed directory: $path", Notice::debug);
if(is_dir($backupPath)) {
if($fileTools->rmdir($backupPath, true)) $this->message("Removed directory: $backupPath", Notice::debug);
}
$files = array();
} else {
$this->error("Failed to remove directory: $path", Notice::debug);
}
}
}
// remove module files individually
foreach($files as $file) {
$file = "$path/$file";
if(!file_exists($file)) continue;
if($fileTools->unlink($file, $siteModulesPath)) {
$this->message("Removed file: $file", Notice::debug);
} else {
$this->error("Unable to remove file: $file", Notice::debug);
}
}
$this->log("Deleted module '$class'");
return $success;
}
/**
* Uninstall the given module name
*
* #pw-group-manipulation
*
* @param string $class Module name (class name)
* @return bool
* @throws WireException
*
*/
public function uninstall($class) {
$class = $this->modules->getModuleClass($class);
$reason = $this->modules->isUninstallable($class, true);
if($reason !== true) {
// throw new WireException("$class - Can't Uninstall - $reason");
return false;
}
// check if there are any modules still installed that this one says it is responsible for installing
foreach($this->getUninstalls($class) as $name) {
// catch uninstall exceptions at this point since original module has already been uninstalled
$label = $this->_('Module Auto Uninstall');
try {
$this->modules->uninstall($name);
$this->message("$label: $name");
} catch(\Exception $e) {
$error = "$label: $name - " . $e->getMessage();
$this->trackException($e, false, $error);
}
}
$info = $this->modules->info->getModuleInfoVerbose($class);
$module = $this->modules->getModule($class, array(
'noPermissionCheck' => true,
'noInstall' => true,
// 'noInit' => true
));
if(!$module) return false;
// remove all hooks attached to this module
$hooks = $module instanceof Wire ? $module->getHooks() : array();
foreach($hooks as $hook) {
if($hook['method'] == 'uninstall') continue;
$this->message("Removed hook $class => " . $hook['options']['fromClass'] . " $hook[method]", Notice::debug);
$module->removeHook($hook['id']);
}
// remove all hooks attached to other ProcessWire objects
$hooks = array_merge($this->getHooks('*'), $this->wire()->hooks->getAllLocalHooks());
foreach($hooks as $hook) {
/** @var Wire $toObject */
$toObject = $hook['toObject'];
$toClass = wireClassName($toObject, false);
$toMethod = $hook['toMethod'];
if($class === $toClass && $toMethod != 'uninstall') {
$toObject->removeHook($hook['id']);
$this->message("Removed hook $class => " . $hook['options']['fromClass'] . " $hook[method]", Notice::debug);
}
}
if(method_exists($module, '___uninstall') || method_exists($module, 'uninstall')) {
// note module's uninstall method may throw an exception to abort the uninstall
/** @var _Module $module */
$module->uninstall();
}
$database = $this->wire()->database;
$query = $database->prepare('DELETE FROM modules WHERE class=:class LIMIT 1'); // QA
$query->bindValue(":class", $class, \PDO::PARAM_STR);
$query->execute();
// add back to the installable list
if(class_exists("ReflectionClass")) {
$reflector = new \ReflectionClass($this->modules->getModuleClass($module, true));
$this->modules->installableFile($class, $reflector->getFileName());
}
$this->modules->moduleID($class, false);
$this->modules->remove($module);
$sanitizer = $this->wire()->sanitizer;
$permissions = $this->wire()->permissions;
// delete permissions installed by this module
if(isset($info['permissions']) && is_array($info['permissions'])) {
foreach($info['permissions'] as $name => $title) {
$name = $sanitizer->pageName($name);
if(ctype_digit("$name") || empty($name)) continue;
$permission = $permissions->get($name);
if(!$permission->id) continue;
try {
$permissions->delete($permission);
$this->message(sprintf($this->_('Deleted Permission: %s'), $name));
} catch(\Exception $e) {
$error = sprintf($this->_('Error deleting permission: %s'), $name);
$this->trackException($e, false, $error);
}
}
}
$this->log("Uninstalled module '$class'");
$this->modules->refresh();
return true;
}
/**
* Return an array of other module class names that are uninstalled when the given one is
*
* #pw-internal
*
* The opposite of this function is found in the getModuleInfo array property 'installs'.
* Note that 'installs' and uninstalls may be different, as only modules in the 'installs' list
* that indicate 'requires' for the installer module will be uninstalled.
*
* @param $class
* @return array
*
*/
public function getUninstalls($class) {
$uninstalls = array();
$class = $this->modules->getModuleClass($class);
if(!$class) return $uninstalls;
$info = $this->modules->info->getModuleInfoVerbose($class);
// check if there are any modules still installed that this one says it is responsible for installing
foreach($info['installs'] as $name) {
// if module isn't installed, then great
if(!$this->modules->isInstalled($name)) continue;
// if an 'installs' module doesn't indicate that it requires this one, then leave it installed
$i = $this->modules->info->getModuleInfo($name);
if(!in_array($class, $i['requires'])) continue;
// add it to the uninstalls array
$uninstalls[] = $name;
}
return $uninstalls;
}
/**
* Return an array of module class names that require the given one
*
* #pw-internal
*
* @param string $class
* @param bool $uninstalled Set to true to include modules dependent upon this one, even if they aren't installed.
* @param bool $installs Set to true to exclude modules that indicate their install/uninstall is controlled by $class.
* @return array()
*
*/
public function getRequiredBy($class, $uninstalled = false, $installs = false) {
$class = $this->modules->getModuleClass($class);
$info = $this->modules->info->getModuleInfo($class);
$dependents = array();
foreach($this->modules as $module) {
$c = $this->modules->getModuleClass($module);
if(!$uninstalled && !$this->modules->isInstalled($c)) continue;
$i = $this->modules->info->getModuleInfo($c);
if(!count($i['requires'])) continue;
if($installs && in_array($c, $info['installs'])) continue;
if(in_array($class, $i['requires'])) $dependents[] = $c;
}
return $dependents;
}
/**
* Return an array of module class names required by the given one
*
* Default behavior is to return all listed requirements, whether they are currently met by
* the environment or not. Specify TRUE for the 2nd argument to return only requirements
* that are not currently met.
*
* #pw-internal
*
* @param string $class
* @param bool $onlyMissing Set to true to return only required modules/versions that aren't
* yet installed or don't have the right version. It excludes those that the class says it
* will install (via 'installs' property of getModuleInfo)
* @param null|bool $versions Set to true to always include versions in the returned requirements list.
* Set to null to always exclude versions in requirements list (so only module class names will be there).
* Set to false (which is the default) to include versions only when version is the dependency issue.
* Note versions are already included when the installed version is not adequate.
* @return array of strings each with ModuleName Operator Version, i.e. "ModuleName>=1.0.0"
*
*/
public function getRequires($class, $onlyMissing = false, $versions = false) {
$class = $this->modules->getModuleClass($class);
$info = $this->modules->getModuleInfo($class);
$requires = $info['requires'];
$currentVersion = 0;
// quick exit if arguments permit it
if(!$onlyMissing) {
if($versions) foreach($requires as $key => $value) {
list($operator, $version) = $info['requiresVersions'][$value];
if(empty($version)) continue;
if(ctype_digit("$version")) $version = $this->modules->formatVersion($version);
if(!empty($version)) $requires[$key] .= "$operator$version";
}
return $requires;
}
foreach($requires as $key => $requiresClass) {
if(in_array($requiresClass, $info['installs'])) {
// if this module installs the required class, then we can stop now
// and we assume it's installing the version it wants
unset($requires[$key]);
}
list($operator, $requiresVersion) = $info['requiresVersions'][$requiresClass];
$installed = true;
if($requiresClass == 'PHP') {
$currentVersion = PHP_VERSION;
} else if($requiresClass == 'ProcessWire') {
$currentVersion = $this->wire()->config->version;
} else if($this->modules->isInstalled($requiresClass)) {
if(!$requiresVersion) {
// if no version is specified then requirement is already met
unset($requires[$key]);
continue;
}
$i = $this->modules->getModuleInfo($requiresClass, array('noCache' => true));
$currentVersion = $i['version'];
} else {
// module is not installed
$installed = false;
}
if($installed && $this->versionCompare($currentVersion, $requiresVersion, $operator)) {
// required version is installed
unset($requires[$key]);
} else if(empty($requiresVersion)) {
// just the class name is fine
continue;
} else if(is_null($versions)) {
// request is for no versions to be included (just class names)
$requires[$key] = $requiresClass;
} else {
// update the requires string to clarify what version it requires
if(ctype_digit("$requiresVersion")) $requiresVersion = $this->modules->formatVersion($requiresVersion);
$requires[$key] = "$requiresClass$operator$requiresVersion";
}
}
return $requires;
}
/**
* Compare one module version to another, returning TRUE if they match the $operator or FALSE otherwise
*
* #pw-internal
*
* @param int|string $currentVersion May be a number like 123 or a formatted version like 1.2.3
* @param int|string $requiredVersion May be a number like 123 or a formatted version like 1.2.3
* @param string $operator
* @return bool
*
*/
public function versionCompare($currentVersion, $requiredVersion, $operator) {
if(ctype_digit("$currentVersion") && ctype_digit("$requiredVersion")) {
// integer comparison is ok
$currentVersion = (int) $currentVersion;
$requiredVersion = (int) $requiredVersion;
$result = false;
switch($operator) {
case '=': $result = ($currentVersion == $requiredVersion); break;
case '>': $result = ($currentVersion > $requiredVersion); break;
case '<': $result = ($currentVersion < $requiredVersion); break;
case '>=': $result = ($currentVersion >= $requiredVersion); break;
case '<=': $result = ($currentVersion <= $requiredVersion); break;
case '!=': $result = ($currentVersion != $requiredVersion); break;
}
return $result;
}
// if either version has no periods or only one, like "1.2" then format it to stanard: "1.2.0"
if(substr_count($currentVersion, '.') < 2) $currentVersion = $this->modules->formatVersion($currentVersion);
if(substr_count($requiredVersion, '.') < 2) $requiredVersion = $this->modules->formatVersion($requiredVersion);
return version_compare($currentVersion, $requiredVersion, $operator);
}
/**
* Return an array of module class names required by the given one to be installed before this one.
*
* Excludes modules that are required but already installed.
* Excludes uninstalled modules that $class indicates it handles via it's 'installs' getModuleInfo property.
*
* #pw-internal
*
* @param string $class
* @return array()
*
*/
public function getRequiresForInstall($class) {
return $this->getRequires($class, true);
}
/**
* Return an array of module class names required by the given one to be uninstalled before this one.
*
* Excludes modules that the given one says it handles via it's 'installs' getModuleInfo property.
* Module class names in returned array include operator and version in the string.
*
* #pw-internal
*
* @param string $class
* @return array()
*
*/
public function getRequiresForUninstall($class) {
return $this->getRequiredBy($class, false, true);
}
/**
* Return array of dependency errors for given module name
*
* #pw-internal
*
* @param $moduleName
* @return array If no errors, array will be blank. If errors, array will be of strings (error messages)
*
*/
public function getDependencyErrors($moduleName) {
$moduleName = $this->modules->getModuleClass($moduleName);
$info = $this->modules->getModuleInfo($moduleName);
$errors = array();
if(empty($info['requires'])) return $errors;
foreach($info['requires'] as $requiresName) {
$error = '';
if(!$this->modules->isInstalled($requiresName)) {
$error = $requiresName;
} else if(!empty($info['requiresVersions'][$requiresName])) {
list($operator, $version) = $info['requiresVersions'][$requiresName];
$info2 = $this->modules->getModuleInfo($requiresName);
$requiresVersion = $info2['version'];
if(!empty($version) && !$this->versionCompare($requiresVersion, $version, $operator)) {
$error = "$requiresName $operator $version";
}
}
if($error) $errors[] = sprintf($this->_('Failed module dependency: %s requires %s'), $moduleName, $error);
}
return $errors;
}
/**
* Get URL where an administrator can install given module name
*
* If module is already installed, it returns the URL to edit the module.
*
* @param string $className
* @return string
*
*/
public function getModuleInstallUrl($className) {
if(!is_string($className)) $className = $this->modules->getModuleClass($className);
$className = $this->wire()->sanitizer->fieldName($className);
if($this->modules->isInstalled($className)) return $this->modules->getModuleEditUrl($className);
return $this->wire()->config->urls->admin . "module/installConfirm?name=$className";
}
}

907
wire/core/ModulesLoader.php Normal file
View File

@@ -0,0 +1,907 @@
<?php namespace ProcessWire;
/**
* ProcessWire Modules: Loader
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class ModulesLoader extends ModulesClass {
/**
* Array of moduleName => order to indicate autoload order when necessary
*
* @var array
*
*/
protected $autoloadOrders = array();
/**
* Array of moduleName => condition
*
* Condition can be either an anonymous function or a selector string to be evaluated at ready().
*
*/
protected $conditionalAutoloadModules = array();
/**
* Cache of module information from DB used across multiple calls temporarily by loadPath() method
*
*/
protected $modulesTableCache = array();
/**
* Module created dates indexed by module ID
*
*/
protected $createdDates = array();
/**
* Initialize all the modules that are loaded at boot
*
* #pw-internal
*
* @param null|array|Modules $modules
* @param array $completed
* @param int $level
*
*/
public function triggerInit($modules = null, $completed = array(), $level = 0) {
$debugKey = null;
$debugKey2 = null;
if($this->debug) {
$debugKey = $this->modules->debugTimerStart("triggerInit$level");
$this->message("triggerInit(level=$level)");
}
$queue = array();
if($modules === null) $modules = $this->modules;
foreach($modules as $class => $module) {
if($module instanceof ModulePlaceholder) {
// skip modules that aren't autoload and those that are conditional autoload
if(!$module->autoload) continue;
if(isset($this->conditionalAutoloadModules[$class])) continue;
}
if($this->debug) $debugKey2 = $this->modules->debugTimerStart("triggerInit$level($class)");
$info = $this->modules->getModuleInfo($module);
$skip = false;
// module requires other modules
foreach($info['requires'] as $requiresClass) {
if(in_array($requiresClass, $completed)) continue;
$dependencyInfo = $this->modules->getModuleInfo($requiresClass);
if(empty($dependencyInfo['autoload'])) {
// if dependency isn't an autoload one, there's no point in waiting for it
if($this->debug) $this->warning("Autoload module '$module' requires a non-autoload module '$requiresClass'");
continue;
} else if(isset($this->conditionalAutoloadModules[$requiresClass])) {
// autoload module requires another autoload module that may or may not load
if($this->debug) $this->warning("Autoload module '$module' requires a conditionally autoloaded module '$requiresClass'");
continue;
}
// dependency is autoload and required by this module, so queue this module to init later
$queue[$class] = $module;
$skip = true;
break;
}
if(!$skip) {
if($info['autoload'] !== false) {
if($info['autoload'] === true || $this->modules->isAutoload($module)) {
$this->initModule($module);
}
}
$completed[] = $class;
}
if($this->debug) $this->modules->debugTimerStop($debugKey2);
}
// if there is a dependency queue, go recursive till the queue is completed
if(count($queue) && $level < 3) {
$this->triggerInit($queue, $completed, $level + 1);
}
$this->modules->isInitialized(true);
if($this->debug) if($debugKey) $this->modules->debugTimerStop($debugKey);
if(!$level && $this->modules->info->moduleInfoCacheEmpty()) {
if($this->debug) $this->message("saveModuleInfoCache from triggerInit");
$this->modules->info->saveModuleInfoCache();
}
}
/**
* Initialize a single module
*
* @param Module $module
* @param array $options
* - `clearSettings` (bool): When true, module settings will be cleared when appropriate to save space. (default=true)
* - `configOnly` (bool): When true, module init() method NOT called, but config data still set (default=false) 3.0.169+
* - `configData` (array): Extra config data merge with modules config data (default=[]) 3.0.169+
* - `throw` (bool): When true, exceptions will be allowed to pass through. (default=false)
* @return bool True on success, false on fail
* @throws \Exception Only if the `throw` option is true.
*
*/
public function initModule(Module $module, array $options = array()) {
$result = true;
$debugKey = null;
$clearSettings = isset($options['clearSettings']) ? (bool) $options['clearSettings'] : true;
$throw = isset($options['throw']) ? (bool) $options['throw'] : false;
if($this->debug) {
static $n = 0;
$this->message("initModule (" . (++$n) . "): " . wireClassName($module));
}
// if the module is configurable, then load its config data
// and set values for each before initializing the module
$extraConfigData = isset($options['configData']) ? $options['configData'] : null;
$this->modules->configs->setModuleConfigData($module, null, $extraConfigData);
$moduleName = wireClassName($module, false);
$moduleID = $this->modules->moduleID($moduleName);
if($moduleID && $this->modules->info->modulesLastVersions($moduleID)) {
$this->modules->info->checkModuleVersion($module);
}
if(method_exists($module, 'init') && empty($options['configOnly'])) {
if($this->debug) {
$debugKey = $this->modules->debugTimerStart("initModule($moduleName)");
}
try {
$module->init();
} catch(\Exception $e) {
if($throw) throw($e);
$this->error(sprintf($this->_('Failed to init module: %s'), $moduleName) . " - " . $e->getMessage());
$result = false;
}
if($this->debug) {
$this->modules->debugTimerStop($debugKey);
}
}
// if module is autoload (assumed here) and singular, then
// we no longer need the module's config data, so remove it
if($clearSettings && $this->modules->isSingular($module)) {
if(!$moduleID) $moduleID = $this->modules->getModuleID($module);
if($moduleID && $this->modules->configs->configData($moduleID) !== null) {
$this->modules->configs->configData($moduleID, 1);
}
}
return $result;
}
/**
* Call ready for a single module
*
* @param Module $module
* @return bool
*
*/
public function readyModule(Module $module) {
$result = true;
if(method_exists($module, 'ready')) {
$debugKey = $this->debug ? $this->modules->debugTimerStart("readyModule(" . $module->className() . ")") : null;
try {
$module->ready();
} catch(\Exception $e) {
$this->error(sprintf($this->_('Failed to ready module: %s'), $module->className()) . " - " . $e->getMessage());
$result = false;
}
if($this->debug) {
$this->modules->debugTimerStop($debugKey);
static $n = 0;
$this->message("readyModule (" . (++$n) . "): " . wireClassName($module));
}
}
return $result;
}
/**
* Trigger all modules 'ready' method, if they have it.
*
* This is to indicate to them that the API environment is fully ready and $page is in fuel.
*
* This is triggered by ProcessPageView::ready
*
* #pw-internal
*
*/
public function triggerReady() {
$debugKey = $this->debug ? $this->modules->debugTimerStart("triggerReady") : null;
$skipped = $this->triggerConditionalAutoload();
// trigger ready method on all applicable modules
foreach($this->modules as $module) {
/** @var Module $module */
if($module instanceof ModulePlaceholder) continue;
$class = $this->modules->getModuleClass($module);
if(isset($skipped[$class])) continue;
$id = $this->modules->moduleID($class);
$flags = $this->modules->flags->moduleFlags($id);
if($flags & Modules::flagsAutoload) $this->readyModule($module);
}
if($this->debug) $this->modules->debugTimerStop($debugKey);
}
/**
* Init conditional autoload modules, if conditions allow
*
* @return array of skipped module names
*
*/
public function triggerConditionalAutoload() {
// conditional autoload modules that are skipped (className => 1)
$skipped = array();
// init conditional autoload modules, now that $page is known
foreach($this->conditionalAutoloadModules as $className => $func) {
if($this->debug) {
$moduleID = $this->modules->getModuleID($className);
$flags = $this->modules->flags->moduleFlags($moduleID);
$this->message("Conditional autoload: $className (flags=$flags, condition=" . (is_string($func) ? $func : 'func') . ")");
}
$load = true;
if(is_string($func)) {
// selector string
if(!$this->wire()->page->is($func)) $load = false;
} else {
// anonymous function
if(!is_callable($func)) $load = false;
else if(!$func()) $load = false;
}
if($load) {
$module = $this->modules->newModule($className);
if($module) {
$this->modules->set($className, $module);
if($this->initModule($module)) {
if($this->debug) $this->message("Conditional autoload: $className LOADED");
} else {
if($this->debug) $this->warning("Failed conditional autoload: $className");
}
}
} else {
$skipped[$className] = $className;
if($this->debug) $this->message("Conditional autoload: $className SKIPPED");
}
}
// clear this out since we don't need it anymore
$this->conditionalAutoloadModules = array();
return $skipped;
}
/**
* Retrieve the installed module info as stored in the database
*
*/
public function loadModulesTable() {
$this->autoloadOrders = array();
$database = $this->wire()->database;
// skip loading dymanic caches at this stage
$skipCaches = array(
ModulesInfo::moduleInfoCacheUninstalledName,
ModulesInfo::moduleInfoCacheVerboseName
);
$query = $database->query(
// Currently: id, class, flags, data, with created added at sysupdate 7
"SELECT * FROM modules " .
"WHERE class NOT IN('" . implode("','", $skipCaches) . "') " .
"ORDER BY class",
"modules.loadModulesTable()"
);
/** @noinspection PhpAssignmentInConditionInspection */
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
$moduleID = (int) $row['id'];
$flags = (int) $row['flags'];
$class = $row['class'];
if($flags & Modules::flagsSystemCache) {
// system cache names are prefixed with a '.' so they load first
$this->modules->memcache(ltrim($class, '.'), $row['data']);
continue;
}
$this->modules->moduleID($class, $moduleID);
$this->modules->moduleName($moduleID, $class);
$this->modules->flags->moduleFlags($moduleID, $flags);
$autoload = $flags & Modules::flagsAutoload;
$loadSettings = $autoload || ($flags & Modules::flagsDuplicate) || ($class === 'SystemUpdater');
if($loadSettings) {
// preload config data for autoload modules since we'll need it again very soon
$data = $row['data'] ? json_decode($row['data'], true) : array();
$this->modules->configs->configData($moduleID, $data);
// populate information about duplicates, if applicable
if($flags & Modules::flagsDuplicate) $this->modules->duplicates()->addFromConfigData($class, $data);
} else if(!empty($row['data'])) {
// indicate that it has config data, but not yet loaded
$this->modules->configs->configData($moduleID, 1);
}
if(isset($row['created']) && $row['created'] != '0000-00-00 00:00:00') {
$this->createdDates[$moduleID] = $row['created'];
}
if($autoload) {
$value = $this->modules->info->moduleInfoCache($moduleID, 'autoload');
if(!empty($value)) {
$autoload = $value;
$disabled = $flags & Modules::flagsDisabled;
if(is_int($autoload) && $autoload > 1 && !$disabled) {
// autoload specifies an order > 1, indicating it should load before others
$this->autoloadOrders[$class] = $autoload;
}
}
}
unset($row['data'], $row['created']); // info we don't want stored in modulesTableCache
$this->modulesTableCache[$class] = $row;
}
$query->closeCursor();
}
/**
* Given a disk path to the modules, determine all installed modules and keep track of all uninstalled (installable) modules.
*
* @param string $path
*
*/
public function loadPath($path) {
$config = $this->wire()->config;
$debugKey = $this->debug ? $this->modules->debugTimerStart("loadPath($path)") : null;
$installed =& $this->modulesTableCache;
$modulesLoaded = array();
$modulesDelayed = array();
$modulesRequired = array();
$modulesFiles = $this->modules->files;
$rootPath = $config->paths->root;
$basePath = substr($path, strlen($rootPath));
foreach($modulesFiles->findModuleFiles($path, true) as $pathname) {
$pathname = trim($pathname);
if(empty($pathname)) continue;
$basename = basename($pathname);
list($moduleName, $ext) = explode('.', $basename, 2); // i.e. "module.php" or "module"
$modulesFiles->moduleFileExt($moduleName, $ext === 'module' ? 1 : 2);
// @todo next, remove the 'file' property from verbose module info since it is redundant
$requires = array();
$name = $moduleName;
$moduleName = $this->loadModule($path, $pathname, $requires, $installed);
if(!$config->paths->__isset($name)) $modulesFiles->setConfigPaths($name, dirname($basePath . $pathname));
if(!$moduleName) continue;
if(count($requires)) {
// module not loaded because it required other module(s) not yet loaded
foreach($requires as $requiresModuleName) {
if(!isset($modulesRequired[$requiresModuleName])) $modulesRequired[$requiresModuleName] = array();
if(!isset($modulesDelayed[$moduleName])) $modulesDelayed[$moduleName] = array();
// queue module for later load
$modulesRequired[$requiresModuleName][$moduleName] = $pathname;
$modulesDelayed[$moduleName][] = $requiresModuleName;
}
continue;
}
// module was successfully loaded
$modulesLoaded[$moduleName] = 1;
$loadedNames = array($moduleName);
// now determine if this module had any other modules waiting on it as a dependency
/** @noinspection PhpAssignmentInConditionInspection */
while($moduleName = array_shift($loadedNames)) {
// iternate through delayed modules that require this one
if(empty($modulesRequired[$moduleName])) continue;
foreach($modulesRequired[$moduleName] as $delayedName => $delayedPathName) {
$loadNow = true;
if(isset($modulesDelayed[$delayedName])) {
foreach($modulesDelayed[$delayedName] as $requiresModuleName) {
if(!isset($modulesLoaded[$requiresModuleName])) {
$loadNow = false;
}
}
}
if(!$loadNow) continue;
// all conditions satisified to load delayed module
unset($modulesDelayed[$delayedName], $modulesRequired[$moduleName][$delayedName]);
$unused = array();
$loadedName = $this->loadModule($path, $delayedPathName, $unused, $installed);
if(!$loadedName) continue;
$modulesLoaded[$loadedName] = 1;
$loadedNames[] = $loadedName;
}
}
}
if(count($modulesDelayed)) {
foreach($modulesDelayed as $moduleName => $requiredNames) {
$this->error("Module '$moduleName' dependency not fulfilled for: " . implode(', ', $requiredNames), Notice::debug);
}
}
if($this->debug) $this->modules->debugTimerStop($debugKey);
}
/**
* Load a module into memory (companion to load bootstrap method)
*
* @param string $basepath Base path of modules being processed (path provided to the load method)
* @param string $pathname
* @param array $requires This method will populate this array with required dependencies (class names) if present.
* @param array $installed Array of installed modules info, indexed by module class name
* @return string Returns module name (classname)
*
*/
public function loadModule($basepath, $pathname, array &$requires, array &$installed) {
$pathname = $basepath . $pathname;
$dirname = dirname($pathname);
$filename = basename($pathname);
$basename = basename($filename, '.php');
$basename = basename($basename, '.module');
$requires = array();
$duplicates = $this->modules->duplicates();
// check if module has duplicate files, where one to use has already been specified to use first
$currentFile = $duplicates->getCurrent($basename); // returns the current file in use, if more than one
if($currentFile) {
// there is a duplicate file in use
$file = rtrim($this->wire()->config->paths->root, '/') . $currentFile;
if(file_exists($file) && $pathname != $file) {
// file in use is different from the file we are looking at
// check if this is a new/yet unknown duplicate
if(!$duplicates->hasDuplicate($basename, $pathname)) {
// new duplicate
$duplicates->recordDuplicate($basename, $pathname, $file, $installed);
}
return '';
}
}
// check if module has already been loaded, or maybe we've got duplicates
if(wireClassExists($basename, false)) {
$module = $this->modules->offsetGet($basename);
$dir = rtrim((string) $this->wire()->config->paths($basename), '/');
if($module && $dir && $dirname != $dir) {
$duplicates->recordDuplicate($basename, $pathname, "$dir/$filename", $installed);
return '';
}
if($module) return $basename;
}
// if the filename doesn't end with .module or .module.php, then stop and move onto the next
if(strpos($filename, '.module') === false) return false;
list(, $ext) = explode('.module', $filename, 2);
if(!empty($ext) && $ext !== '.php') return false;
// if the filename doesn't start with the requested path, then skip
if(strpos($pathname, $basepath) !== 0) return '';
// if the file isn't there, it was probably uninstalled, so ignore it
// if(!file_exists($pathname)) return '';
// if the module isn't installed, then stop and move on to next
if(!isset($installed[$basename])) {
// array_key_exists is used as secondary to check the null case
$this->modules->installableFile($basename, $pathname);
return '';
}
$info = $installed[$basename];
$this->modules->files->setConfigPaths($basename, $dirname);
$module = null;
$autoload = false;
if($info['flags'] & Modules::flagsAutoload) {
// this is an Autoload module.
// include the module and instantiate it but don't init() it,
// because it will be done by Modules::init()
// determine if module has dependencies that are not yet met
$requiresClasses = $this->modules->info->getModuleInfoProperty($basename, 'requires');
if(!empty($requiresClasses)) {
foreach($requiresClasses as $requiresClass) {
$nsRequiresClass = $this->modules->getModuleClass($requiresClass, true);
if(!wireClassExists($nsRequiresClass, false)) {
$requiresInfo = $this->modules->getModuleInfo($requiresClass);
if(!empty($requiresInfo['error'])
|| $requiresInfo['autoload'] === true
|| !$this->modules->isInstalled($requiresClass)) {
// we only handle autoload===true since load() only instantiates other autoload===true modules
$requires[] = $requiresClass;
}
}
}
if(count($requires)) {
// module has unmet requirements
return $basename;
}
}
// if not defined in getModuleInfo, then we'll accept the database flag as enough proof
// since the module may have defined it via an isAutoload() function
/** @var bool|string|callable $autoload */
$autoload = $this->modules->info->moduleInfoCache($basename, 'autoload');
if(empty($autoload)) $autoload = true;
if($autoload === 'function') {
// function is stored by the moduleInfo cache to indicate we need to call a dynamic function specified with the module itself
$i = $this->modules->info->getModuleInfoExternal($basename);
if(empty($i)) {
$this->modules->files->includeModuleFile($pathname, $basename);
$namespace = $this->modules->info->getModuleNamespace($basename);
$className = $namespace . $basename;
if(method_exists($className, 'getModuleInfo')) {
$i = $className::getModuleInfo();
} else {
$i = $this->modules->getModuleInfo($className);
}
}
$autoload = isset($i['autoload']) ? $i['autoload'] : true;
unset($i);
}
// check for conditional autoload
if(!is_bool($autoload) && (is_string($autoload) || is_callable($autoload)) && !($info['flags'] & Modules::flagsDisabled)) {
// anonymous function or selector string
$this->conditionalAutoloadModules[$basename] = $autoload;
$this->modules->moduleID($basename, (int) $info['id']);
$this->modules->moduleName((int) $info['id'], $basename);
$autoload = true;
} else if($autoload) {
$this->modules->files->includeModuleFile($pathname, $basename);
if(!($info['flags'] & Modules::flagsDisabled)) {
if($this->modules->refreshing) {
$module = $this->modules->offsetGet($basename);
} else if(isset($this->autoloadOrders[$basename]) && $this->autoloadOrders[$basename] >= 10000) {
$module = $this->modules->offsetGet($basename); // preloaded module
}
if(!$module) $module = $this->modules->newModule($basename);
}
}
}
if($module === null) {
// placeholder for a module, which is not yet included and instantiated
$ns = $this->modules->info->moduleInfoCache($basename, 'namespace');
if(empty($ns)) $ns = __NAMESPACE__ . "\\";
$singular = $info['flags'] & Modules::flagsSingular;
$module = $this->newModulePlaceholder($basename, $ns, $pathname, $singular, $autoload);
}
$this->modules->moduleID($basename, (int) $info['id']);
$this->modules->moduleName((int) $info['id'], $basename);
$this->modules->set($basename, $module);
return $basename;
}
/**
* Include the file for a given module, but don't instantiate it
*
* #pw-internal
*
* @param ModulePlaceholder|Module|string Expects a ModulePlaceholder or className
* @param string $file Optionally specify the module filename if you already know it
* @return bool true on success, false on fail or unknown
*
*/
public function includeModule($module, $file = '') {
$className = '';
$moduleName = '';
if(is_string($module)) {
$moduleName = ctype_alnum($module) ? $module : wireClassName($module);
$className = wireClassName($module, true);
} else if(is_object($module)) {
if($module instanceof ModulePlaceholder) {
$moduleName = $module->className();
$className = $module->className(true);
} else if($module instanceof Module) {
return true; // already included
}
} else {
$moduleName = $this->modules->getModuleClass($module, false);
$className = $this->modules->getModuleClass($module, true);
}
if(!$className) return false;
// already included
if(class_exists($className, false)) return true;
// attempt to retrieve module
$module = $this->modules->offsetGet($moduleName);
if($module) {
// module found, check to make sure it actually points to a module
if(!$module instanceof Module) $module = false;
} else if($moduleName) {
// This is reached for any of the following:
// 1. an uninstalled module
// 2. an installed module that has changed locations
// 3. a module outside the \ProcessWire\ namespace
// 4. a module that does not exist
$fast = true;
if(!$file) {
// determine module file, if not already provided to the method
$file = $this->modules->getModuleFile($moduleName, array('fast' => true));
if(!$file) {
$fast = false;
$file = $this->modules->getModuleFile($moduleName, array('fast' => false));
}
// still can't figure out what file is? fail
if(!$file) return false;
}
if(!$this->modules->files->includeModuleFile($file, $moduleName)) {
// module file failed to include(), try to identify and include file again
if($fast) {
$filePrev = $file;
$file = $this->modules->getModuleFile($moduleName, array('fast' => false));
if($file && $file !== $filePrev) {
if($this->modules->files->includeModuleFile($file, $moduleName)) {
// module is missing a module file
return false;
}
}
} else {
// we already tried this earlier, no point in doing it again
}
}
// now check to see if included file resulted in presence of module class
if(class_exists($className)) {
// module in ProcessWire namespace
$module = true;
} else {
// module in root namespace or some other namespace
$namespace = (string) $this->modules->info->getModuleNamespace($moduleName, array('file' => $file));
$className = trim($namespace, "\\") . "\\$moduleName";
if(class_exists($className, false)) {
// successful include module
$module = true;
}
}
}
if($module === true) {
// great
return true;
} else if(!$module) {
// darn
return false;
} else if($module instanceof ModulePlaceholder) {
// the ModulePlaceholder indicates what file to load
return $this->modules->files->includeModuleFile($module->file, $moduleName);
} else if($module instanceof Module) {
// it's already been included, since we have a real module
return true;
} else {
return false;
}
}
/**
* Check if user has permission for given module
*
* #pw-internal
*
* @param string|object $moduleName Module instance or module name
* @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
* (though this method doesn't get involved in that, leaving you to specify $strict instead).
*
* @return bool
*
*/
public function hasPermission($moduleName, ?User $user = null, ?Page $page = null, $strict = false) {
if(is_object($moduleName)) {
$module = $moduleName;
$className = $module->className(true);
$moduleName = $module->className(false);
} else {
$module = null;
$className = $this->modules->getModuleClass($moduleName, true); // ???
$moduleName = wireClassName($moduleName, false);
}
$info = $this->modules->getModuleInfo($module ? $module : $moduleName);
if(empty($info['permission']) && empty($info['permissionMethod'])) {
return ($strict ? false : true);
}
if(!$user instanceof User) $user = $this->wire()->user;
if($user && $user->isSuperuser()) return true;
if(!empty($info['permission'])) {
if(!$user->hasPermission($info['permission'])) return false;
}
if(!empty($info['permissionMethod'])) {
// module specifies a static method to call for permission
if(is_null($page)) $page = $this->wire()->page;
$data = array(
'wire' => $this->wire(),
'page' => $page,
'user' => $user,
'info' => $info,
);
$method = $info['permissionMethod'];
$this->includeModule($moduleName);
return method_exists($className, $method) ? $className::$method($data) : false;
}
return true;
}
/**
* Include site preload modules
*
* Preload modules load before all other modules, including core modules. In order
* for a module to be a preload module, it must meet the following conditions:
*
* - Module info `autoload` value is integer of 10000 or greater, i.e. `[ 'autoload' => 10000 ]`
* - Module info `singular` value must be non-empty, i.e. `[ 'singular' => true ]`
* - Module file is located in: /site/modules/ModuleName/ModuleName.module.php
* - Module cannot load any other modules at least until ready() method called.
* - Module cannot have any `requires` dependencies to any other modules.
*
* Please note the above is specifically stating that the module must be in its
* own “site/ModuleName/” directory and have the “.module.php” extension. Using
* just the “.module” extension is not supported for preload modules.
*
* @param string $path
* @since 3.0.173
*
*/
public function preloadModules($path) {
if(empty($this->autoloadOrders)) return;
arsort($this->autoloadOrders);
foreach($this->autoloadOrders as $moduleName => $order) {
if($order < 10000) break;
$info = $this->modules->info->moduleInfoCache($moduleName);
if(empty($info)) continue;
if(empty($info['singular'])) continue;
$file = $path . "$moduleName/$moduleName.module.php";
if(!file_exists($file) || !$this->modules->files->includeModuleFile($file, $moduleName)) continue;
if(!isset($info['namespace'])) $info['namespace'] = '';
$className = $info['namespace'] . $moduleName;
$module = $this->modules->newModule($className, $moduleName);
if($module) {
$this->modules->offsetSet($moduleName, $module);
}
}
}
/**
* Get or set created date for given module ID
*
* #pw-internal
*
* @param int $moduleID Module ID or omit to get all
* @param string $setValue Set created date value
* @return string|array|null
* @since 3.0.219
*
*/
public function createdDate($moduleID = null, $setValue = null) {
if($moduleID === null) return $this->createdDates;
if($setValue) {
$this->createdDates[$moduleID] = $setValue;
return $setValue;
}
return isset($this->createdDates[$moduleID]) ? $this->createdDates[$moduleID] : null;
}
/**
* Return a new ModulePlaceholder for the given className
*
* #pw-internal
*
* @param string $className Module class this placeholder will stand in for
* @param string $ns Module namespace
* @param string $file Full path and filename of $className
* @param bool $singular Is the module a singular module?
* @param bool $autoload Is the module an autoload module?
* @return ModulePlaceholder
*
*/
public function newModulePlaceholder($className, $ns, $file, $singular, $autoload) {
/** @var ModulePlaceholder $module */
$module = $this->wire(new ModulePlaceholder());
$module->setClass($className);
$module->setNamespace($ns);
$module->singular = $singular;
$module->autoload = $autoload;
$module->file = $file;
return $module;
}
/**
* Called by Modules class when init has finished
*
*/
public function loaded() {
$this->modulesTableCache = array();
}
/**
* Get the autoload orders
*
* @return array Array of [ moduleName (string => order (int) ]
*
*/
public function getAutoloadOrders() {
return $this->autoloadOrders;
}
public function getDebugData() {
return array(
'autoloadOrders' => $this->autoloadOrders,
'conditionalAutoloadModules' => $this->conditionalAutoloadModules,
'modulesTableCache' => $this->modulesTableCache,
'createdDates' => $this->createdDates,
);
}
}

View File

@@ -10,10 +10,10 @@
* Base class that holds a message, source class, and timestamp.
* Contains notices/messages used by the application to the user.
*
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
* @property string $text Text of notice
* @property string|object|array $text Text or value of notice
* @property string $class Class of notice
* @property int $timestamp Unix timestamp of when the notice was generated
* @property int $flags Bitmask using any of the Notice::constants
@@ -65,11 +65,19 @@ abstract class Notice extends WireData {
/**
* Flag indicates the notice is allowed to contain markup and wont be automatically entity encoded
*
* Note: entity encoding is done by the admin theme at output time, which should detect this flag.
*
* Note: entity encoding is done by the admin theme at output time, which should detect this flag.
*
*/
const allowMarkup = 32;
/**
* Alias of allowMarkup flag
*
* @since 3.0.208
*
*/
const markup = 32;
/**
* Make notice anonymous (not tied to a particular class)
*
@@ -86,6 +94,14 @@ abstract class Notice extends WireData {
*/
const noGroup = 131072;
/**
* Alias of noGroup flag
*
* @since 3.0.208
*
*/
const separate = 131072;
/**
* Ignore notice unless it will be seen by a logged-in user
*
@@ -131,6 +147,32 @@ abstract class Notice extends WireData {
*/
const allowMarkdown = 4194304;
/**
* Alias of allowMarkdown flag
*
* @since 3.0.208
*
*/
const markdown = 4194304;
/**
* Present duplicate notices separately rather than collapsing them to one
*
* String name can be referred to as 'allowDuplicate' or just 'duplicate'
*
* @since 3.0.208
*
*/
const allowDuplicate = 8388608;
/**
* Alias of allowDuplicate flag
*
* @since 3.0.208
*
*/
const duplicate = 8388608;
/**
* Flag integers to flag names
*
@@ -145,6 +187,7 @@ abstract class Notice extends WireData {
self::logOnly => 'logOnly',
self::allowMarkup => 'allowMarkup',
self::allowMarkdown => 'allowMarkdown',
self::allowDuplicate => 'allowDuplicate',
self::anonymous => 'anonymous',
self::noGroup => 'noGroup',
self::login => 'login',
@@ -152,6 +195,20 @@ abstract class Notice extends WireData {
self::superuser => 'superuser',
self::persist => 'persist',
);
/**
* Alternate names to flags
*
* @var int[]
* @since 3.0.208
*
*/
static protected $flagNamesAlt = array(
'duplicate' => self::allowDuplicate,
'markup' => self::allowMarkup,
'markdown' => self::allowMarkdown,
'separate' => self::noGroup,
);
/**
* Create the Notice
@@ -186,7 +243,7 @@ abstract class Notice extends WireData {
if($key === 'text' && is_string($value) && strpos($value, 'icon-') === 0 && strpos($value, ' ')) {
list($icon, $value) = explode(' ', $value, 2);
list(,$icon) = explode('-', $icon, 2);
$icon = $this->wire('sanitizer')->name($icon);
$icon = $this->wire()->sanitizer->name($icon);
if(strlen($icon)) $this->set('icon', $icon);
} else if($key === 'flags') {
$this->flags($value);
@@ -258,7 +315,15 @@ abstract class Notice extends WireData {
if(is_int($name)) return $name;
$name = trim($name);
if(ctype_digit("$name")) return (int) $name;
$flag = array_search(strtolower($name), array_map('strtolower', self::$flagNames));
$name = strtolower($name);
if(isset(self::$flagNamesAlt[$name])) {
return self::$flagNamesAlt[$name];
} else if(strpos($name, 'icon-') === 0) {
$this->icon = substr($name, 5);
$flag = 0;
} else {
$flag = array_search($name, array_map('strtolower', self::$flagNames));
}
return $flag ? $flag : 0;
}
@@ -348,7 +413,16 @@ abstract class Notice extends WireData {
}
public function __toString() {
return (string) $this->text;
$text = $this->text;
if(is_object($text)) {
$value = method_exists($text, '__toString') ? (string) $text : '';
$class = $text->className();
$text = "object:$class";
if($value !== '' && $value !== $class) $text .= "($value)";
} else if(is_array($text)) {
$text = 'array(' . count($text) . ')';
}
return $text;
}
}
@@ -433,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
@@ -474,11 +554,12 @@ class Notices extends WireArray {
*
*/
protected function allowNotice(Notice $item) {
$user = $this->wire('user'); /** @var User $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;
if(!$this->wire()->config->debug) return false;
}
if($item->flags & Notice::superuser) {
@@ -490,11 +571,13 @@ class Notices extends WireArray {
}
if($item->flags & Notice::admin) {
$page = $this->wire('page'); /** @var Page|null $page */
$page = $this->wire()->page;
if(!$page || !$page->template || $page->template->name != 'admin') return false;
}
if($this->isDuplicate($item)) {
if($item->flags & Notice::allowDuplicate) {
// allow it
} else if($this->isDuplicate($item)) {
$item->qty = $item->qty+1;
return false;
}
@@ -516,20 +599,46 @@ class Notices extends WireArray {
*/
protected function formatNotice(Notice $item) {
$text = $item->text;
$label = '';
if(is_array($text)) {
$item->text = "<pre>" . trim(print_r($this->sanitizeArray($text), true)) . "</pre>";
// if text is associative array with 1 item, we consider the
// key to be the notice label and value to be the notice text
if(count($text) === 1) {
$value = reset($text);
$key = key($text);
if(is_string($key)) {
$label = $key;
$text = $value;
$item->text = $text;
if($this->wire()->config->debug) {
$item->class = $label;
$label = '';
}
}
}
}
if(is_object($text) || is_array($text)) {
$text = Debug::toStr($text, array('html' => true));
$item->flags = $item->flags | Notice::allowMarkup;
} else if(is_object($text) && $text instanceof Wire) {
$item->text = "<pre>" . $this->wire()->sanitizer->entities(print_r($text, true)) . "</pre>";
$item->flags = $item->flags | Notice::allowMarkup;
} else if(is_object($text)) {
$item->text = (string) $text;
}
$item->text = $text;
}
if($item->hasFlag('allowMarkdown')) {
$item->text = $this->wire()->sanitizer->entitiesMarkdown($text, array('allowBrackets' => true));
$item->addFlag('allowMarkup');
$item->removeFlag('allowMarkdown');
}
if($label) {
if($item->hasFlag('allowMarkup')) {
$label = $this->wire()->sanitizer->entities($label);
$item->text = "<strong>$label:</strong> $item->text";
} else {
$item->text = "$label: \n$item->text";
}
}
}
/**
@@ -577,8 +686,7 @@ class Notices extends WireArray {
*
*/
protected function storeNotice(Notice $item) {
/** @var Session $session */
$session = $this->wire('session');
$session = $this->wire()->session;
if(!$session) return false;
$items = $session->getFor($this, 'items');
if(!is_array($items)) $items = array();
@@ -598,7 +706,7 @@ class Notices extends WireArray {
*/
protected function loadStoredNotices() {
$session = $this->wire('session');
$session = $this->wire()->session;
$items = $session->getFor($this, 'items');
$qty = 0;
@@ -640,7 +748,7 @@ class Notices extends WireArray {
return $this;
}
if($item) parent::remove($item);
$session = $this->wire('session');
$session = $this->wire()->session;
$items = $session->getFor($this, 'items');
if(is_array($items) && isset($items[$idStr])) {
unset($items[$idStr]);
@@ -680,13 +788,12 @@ class Notices extends WireArray {
*
*/
protected function addLog(Notice $item) {
/** @var Notice $item */
$text = $item->text;
if(strpos($text, '&') !== false) {
$text = $this->wire('sanitizer')->unentities($text);
$text = $this->wire()->sanitizer->unentities($text);
}
if($this->wire('config')->debug && $item->class) $text .= " ($item->class)";
$this->wire('log')->save($item->getName(), $text);
if($this->wire()->config->debug && $item->class) $text .= " ($item->class)";
$this->wire()->log->save($item->getName(), $text);
}
/**
@@ -729,16 +836,24 @@ class Notices extends WireArray {
*
*/
public function sanitizeArray(array $a) {
$sanitizer = $this->wire('sanitizer');
$sanitizer = $this->wire()->sanitizer;
$b = array();
foreach($a as $key => $value) {
if(is_array($value)) {
$value = $this->sanitizeArray($value);
} else {
if(is_object($value)) $value = (string) $value;
if(is_object($value)) {
if($value instanceof Wire) {
$value = (string) $value;
$class = wireClassName($value);
if($value !== $class) $value = "object:$class($value)";
} else {
$value = 'object:' . wireClassName($value);
}
}
$value = $sanitizer->entities($value);
}
$key = $this->wire('sanitizer')->entities($key);
$key = $sanitizer->entities($key);
$b[$key] = $value;
}
return $b;

View File

@@ -25,7 +25,7 @@
* Placeholder class for non-existant and non-saveable Page.
* Many API functions return a NullPage to indicate no match.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @property int $id The id property will always be 0 for a NullPage.
@@ -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; }
@@ -77,7 +78,9 @@ class NullPage extends Page implements WireNull {
* @throws WireException
*
*/
public function parents($selector = '') { return $this->wire('pages')->newPageArray(); }
public function parents($selector = '') {
return $this->wire()->pages->newPageArray();
}
/**
* #pw-internal
@@ -95,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
*
@@ -110,7 +121,22 @@ class NullPage extends Page implements WireNull {
* @throws WireException
*
*/
public function ___rootParent() { return $this->wire('pages')->newNullPage(); }
public function ___rootParent() {
return $this->wire()->pages->newNullPage();
}
/**
* #pw-internal
*
* @param string $selector
* @param bool $includeCurrent
* @return PageArray
* @throws WireException
*
*/
public function siblings($selector = '', $includeCurrent = true) {
return $this->wire()->pages->newPageArray();
}
/**
* #pw-internal
@@ -121,18 +147,9 @@ class NullPage extends Page implements WireNull {
* @throws WireException
*
*/
public function siblings($selector = '', $options = array()) { return $this->wire('pages')->newPageArray(); }
/**
* #pw-internal
*
* @param string $selector
* @param array $options
* @return PageArray
* @throws WireException
*
*/
public function children($selector = '', $options = array()) { return $this->wire('pages')->newPageArray(); }
public function children($selector = '', $options = array()) {
return $this->wire()->pages->newPageArray();
}
/**
* #pw-internal
@@ -142,7 +159,9 @@ class NullPage extends Page implements WireNull {
* @throws WireException
*
*/
public function getAccessParent($type = 'view') { return $this->wire('pages')->newNullPage(); }
public function getAccessParent($type = 'view') {
return $this->wire()->pages->newNullPage();
}
/**
* #pw-internal
@@ -152,7 +171,9 @@ class NullPage extends Page implements WireNull {
* @throws WireException
*
*/
public function getAccessRoles($type = 'view') { return $this->wire('pages')->newPageArray(); }
public function getAccessRoles($type = 'view') {
return $this->wire()->pages->newPageArray();
}
/**
* #pw-internal
@@ -173,4 +194,3 @@ class NullPage extends Page implements WireNull {
*/
public function isChanged($what = '') { return false; }
}

View File

@@ -115,7 +115,7 @@ class PWPNG {
'channels' => $ct,
'bits' => $bpc,
'dp' => $dp,
'palette' => (function_exists('utf8_encode') ? utf8_encode($pal) : $pal),
'palette' => (function_exists('mb_convert_encoding') ? mb_convert_encoding($pal, 'UTF-8', 'ISO-8859-1') : $pal),
'trans' => $trns,
'alpha' => $ct >= 4 ? true : false,
'interlace' => $interlaced,
@@ -149,4 +149,4 @@ class PWPNG {
$this->errors[] = $msg;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
*
* Provides implementation for Page access functions.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
@@ -85,7 +85,7 @@ class PageAccess {
$parent = null;
if($type === 'edit' && $page->isTrash() && $page->id != $page->wire('config')->trashPageID) {
if($type === 'edit' && $page->isTrash() && $page->id != $page->wire()->config->trashPageID) {
// pages in trash have an edit access parent as whatever it was prior to being trashed
$info = $pages->trasher()->parseTrashPageName($page->name);
if(!empty($info['parent_id'])) $parent = $pages->get((int) $info['parent_id']);
@@ -160,9 +160,13 @@ class PageAccess {
*
*/
public function wire($name = '', $value = null, $lock = false) {
if(!is_null($value)) return $this->wire->wire($name, $value, $lock);
else if($name instanceof WireFuelable && $this->wire) $name->setWire($this->wire);
else if($name) return $this->wire->wire($name);
if(!is_null($value)) {
return $this->wire->wire($name, $value, $lock);
} else if($name instanceof WireFuelable && $this->wire) {
$name->setWire($this->wire);
} else if($name) {
return $this->wire->wire($name);
}
return $this->wire;
}

View File

@@ -7,6 +7,9 @@
*
* This file is licensed under the MIT license
* https://processwire.com/about/license/mit/
*
* @method bool action(Page $item)
* @method executeMultiple(PageArray $items)
*
*/
@@ -36,6 +39,17 @@ abstract class PageAction extends WireAction implements Module {
return strlen(__NAMESPACE__) ? __NAMESPACE__ . '\\Page' : 'Page';
}
/**
* Execute the action for the given page
*
* @param Page $item Item to operate upon
* @return bool True if the item was successfully operated upon, false if not.
*
*/
public function execute($item) {
return parent::execute($item);
}
/**
* Perform the action on the given item
*

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
*
@@ -84,7 +94,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
*/
public function isValidItem($item) {
return is_object($item) && $item instanceof Page;
return $item instanceof Page;
}
/**
@@ -123,7 +133,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
// given item exists in this PageArray (or at least has)
$key = $this->keyIndex[$id];
if(isset($this->data[$key])) {
$page = $this->data[$key]; /** @var Page $page */
$page = $this->data[$key];
if($page->id === $id) {
// found it
return $key;
@@ -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
*
@@ -188,17 +186,17 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
* #pw-internal
*
* @param array|PageArray|Page $pages Pages to import.
* @param array|PageArray|Page $items Pages to import.
* @return PageArray reference to current instance.
*
*/
public function import($pages) {
if(is_object($pages) && $pages instanceof Page) $pages = array($pages);
if(!self::iterable($pages)) return $this;
foreach($pages as $page) $this->add($page);
if($pages instanceof PageArray) {
if(count($pages) < $pages->getTotal()) {
$this->setTotal($this->getTotal() + ($pages->getTotal() - count($pages)));
public function import($items) {
if($items instanceof Page) $items = array($items);
if(!self::iterable($items)) return $this;
foreach($items as $page) $this->add($page);
if($items instanceof PageArray) {
if(count($items) < $items->getTotal()) {
$this->setTotal($this->getTotal() + ($items->getTotal() - count($items)));
}
}
return $this;
@@ -213,7 +211,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
* @return bool True if the index or Page exists here, false if not.
*/
public function has($key) {
if(is_object($key) && $key instanceof Page) {
if($key instanceof Page) {
return $this->getItemKey($key) !== null;
}
return parent::has($key);
@@ -236,24 +234,25 @@ class PageArray extends PaginatedArray implements WirePaginatable {
* $pageArray->add(1005);
* ~~~~~
*
* @param Page|PageArray|int $page Page object, PageArray object, or Page ID.
* @param Page|PageArray|int $item Page object, PageArray object, or Page ID.
* - If given a `Page`, the Page will be added.
* - If given a `PageArray`, it will do the same thing as the `WireArray::import()` method and append all the pages.
* - If Page `ID`, the Page identified by that ID will be loaded and added to the PageArray.
* @return $this
*/
public function add($page) {
public function add($item) {
if($this->isValidItem($page)) {
parent::add($page);
if($this->isValidItem($item)) {
parent::add($item);
} else if($page instanceof PageArray || is_array($page)) {
return $this->import($page);
} else if($item instanceof PageArray || is_array($item)) {
return $this->import($item);
} else if(ctype_digit("$page")) {
$page = $this->wire()->pages->get("id=$page");
if($page->id) parent::add($page);
} else if(ctype_digit("$item")) {
$item = $this->wire()->pages->get("id=$item");
if($item->id) parent::add($item);
}
return $this;
}
@@ -285,7 +284,9 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
*/
public function findRandom($num) {
return parent::findRandom($num);
/** @var PageArray $value */
$value = parent::findRandom($num);
return $value;
}
/**
@@ -302,7 +303,9 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
*/
public function slice($start, $limit = 0) {
return parent::slice($start, $limit);
/** @var PageArray $value */
$value = parent::slice($start, $limit);
return $value;
}
/**
@@ -317,7 +320,9 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
*/
public function eq($num) {
return parent::eq($num);
/** @var Page $value */
$value = parent::eq($num);
return $value;
}
/**
@@ -453,7 +458,9 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
*/
public function find($selector) {
return parent::find($selector);
/** @var PageArray $value */
$value = parent::find($selector);
return $value;
}
/**
@@ -467,7 +474,9 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
*/
public function findOne($selector) {
return parent::findOne($selector);
/** @var Page|bool $value */
$value = parent::findOne($selector);
return $value;
}
/**
@@ -610,10 +619,16 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
*/
public function __toString() {
$s = '';
foreach($this as $key => $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);
}
/**
@@ -638,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);
}
}
@@ -698,7 +714,6 @@ class PageArray extends PaginatedArray implements WirePaginatable {
protected function trackAdd($item, $key) {
parent::trackAdd($item, $key);
if(!$item instanceof Page) return;
/** @var Page $item */
if(!isset($this->keyIndex[$item->id])) $this->numTotal++;
$this->keyIndex[$item->id] = $key;
}
@@ -713,12 +728,9 @@ class PageArray extends PaginatedArray implements WirePaginatable {
protected function trackRemove($item, $key) {
parent::trackRemove($item, $key);
if(!$item instanceof Page) return;
/** @var Page $item */
if(isset($this->keyIndex[$item->id])) {
if($this->numTotal) $this->numTotal--;
unset($this->keyIndex[$item->id]);
}
}
}

View File

@@ -5,13 +5,21 @@
*
* Provides implementation for Page comparison functions.
*
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class PageComparison {
/**
* Selector properties that the matches() method ignores
*
* @var string[]
*
*/
protected $matchesIgnores = array('limit', 'start', 'sort', 'include');
/**
* Is this page of the given type? (status, template, etc.)
*
@@ -186,12 +194,14 @@ class PageComparison {
* Given a Selectors object or a selector string, return whether this Page matches it
*
* @param Page $page
* @param string|Selectors $s
* @param string|array|Selectors $s
* @param array $options Options to modify behavior (3.0.225+ only):
* - `useDatabase` (bool|null): Use database for matching rather than in-memory? (default=false)
* @return bool
*
*/
public function matches(Page $page, $s) {
public function matches(Page $page, $s, array $options = array()) {
$selectors = array();
if(is_string($s) || is_int($s)) {
@@ -204,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
@@ -220,47 +230,155 @@ class PageComparison {
}
} else if($s instanceof Selectors) {
$selectors = $s;
$selectors = $s;
} else if(is_array($s)) {
$selectors = $page->wire(new Selectors($s));
} else {
// unknown data type to match
return false;
}
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;
$ignores = array('limit', 'start', 'sort', 'include');
$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, $ignores)) 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;
}
}
return $matches;
// 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 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;
}
/**
* Given an object, return the value(s) it represents (optionally from a property in the object)
*
@@ -367,6 +485,33 @@ class PageComparison {
return $value;
}
/**
* Is $value1 equal to $value2?
*
* @param string $key Name of the key that triggered the check (see WireData::set)
* @param mixed $value1
* @param mixed $value2
* @return bool
*
*/
public function isEqual(Page $page, $key, $value1, $value2) {
$isEqual = $value1 === $value2;
if(!$isEqual && $value1 instanceof WireArray && $value2 instanceof WireArray) {
// ask WireArray to compare itself to another
$isEqual = $value1->isIdentical($value2, true);
}
if($isEqual) {
if($value1 instanceof Wire && ($value1->isChanged() || $value2->isChanged())) {
$page->trackChange($key, $value1, $value2);
}
}
return $isEqual;
}
}

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:
@@ -402,10 +402,10 @@ class PageFinder extends Wire {
// protected $extraJoins = array();
// protected $nativeWheres = array(); // where statements for native fields, to be reused in subselects where appropriate.
public function __get($key) {
if($key === 'includeMode') return $this->includeMode;
if($key === 'checkAccess') return $this->checkAccess;
return parent::__get($key);
public function __get($name) {
if($name === 'includeMode') return $this->includeMode;
if($name === 'checkAccess') return $this->checkAccess;
return parent::__get($name);
}
/**
@@ -455,7 +455,6 @@ class PageFinder extends Wire {
*/
protected function initSelectors(Selectors $selectors, array $options) {
$maxStatus = null;
$limit = 0; // for getTotal auto detection
$start = 0;
$limitSelector = null;
@@ -485,7 +484,6 @@ class PageFinder extends Wire {
);
foreach($selectors as $key => $selector) {
/** @var Selector $selector */
$fieldName = $selector->field;
$operator = $selector->operator;
@@ -690,6 +688,7 @@ class PageFinder extends Wire {
if(($operator === '!=' && !$selector->not) || ($selector->not && $operator === '=')) {
// NOT MATCH condition: replace with bitwise AND NOT selector
/** @var Selector $s */
$s = $this->wire(new SelectorBitwiseAnd('status', $qty > 1 ? $values : reset($values)));
$s->not = true;
$not = true;
@@ -790,6 +789,7 @@ class PageFinder extends Wire {
if(is_string($selectors) || is_array($selectors)) {
list($s, $selectors) = array($selectors, $this->wire(new Selectors()));
/** @var Selectors $selectors */
$selectors->init($s);
} else if(!$selectors instanceof Selectors) {
throw new PageFinderException("find() requires Selectors object, string or array");
@@ -1162,7 +1162,6 @@ class PageFinder extends Wire {
$o['getTotal'] = true;
$o['loadPages'] = false;
$o['returnVerbose'] = false;
/** @var Selectors $sel */
$sel = clone $selectors;
foreach($sel as $s) {
if($s->field == 'limit' || $s->field == 'start') $sel->remove($s);
@@ -1242,7 +1241,7 @@ class PageFinder extends Wire {
$fieldtypeLang = $languages ? $fieldtypes->get("{$fieldName}Language") : null;
foreach($this->fields as $f) {
/** @var Field $f */
if($findExtends) {
// allow any Fieldtype that is an instance of given one, or extends it
if(!wireInstanceOf($f->type, $fieldtype)
@@ -1431,6 +1430,7 @@ class PageFinder extends Wire {
} else {
$s = '';
}
/** @var Selectors $_selectors */
$_selectors = $this->wire(new Selectors($s));
$_selector = $_selectors->create(implode('.', $parts), $selector->operator, $selector->values);
$_selectors->add($_selector);
@@ -1679,7 +1679,7 @@ class PageFinder extends Wire {
$this->pageArrayData['joinFields'] = array(); // identify whether each field supported autojoin
foreach($opts['joinFields'] as $joinField) {
$joinField = $this->fields->get($joinField);
if(!$joinField || !$joinField instanceof Field) continue;
if(!$joinField instanceof Field) continue;
$joinTable = $database->escapeTable($joinField->getTable());
if(!$joinTable || !$joinField->type) continue;
if($joinField->type->getLoadQueryAutojoin($joinField, $query)) {
@@ -1817,6 +1817,7 @@ class PageFinder extends Wire {
$tableAlias = $database->escapeTable($tableAlias);
$join = '';
$joinType = '';
$numEmptyValues = 0;
$valueArray = $selector->values(true);
$fieldtype = $field->type;
@@ -1828,8 +1829,9 @@ class PageFinder extends Wire {
// shortcut for blank value condition: this ensures that NULL/non-existence is considered blank
// without this section the query would still work, but a blank value must actually be present in the field
$isEmptyValue = $fieldtype->isEmptyValue($field, $value);
$useEmpty = $isEmptyValue || $operator[0] === '<' || ((int) $value < 0 && $operator[0] === '>');
if($useEmpty && $fieldtype && strpos($subfield, 'data') === 0) { // && !$fieldtype instanceof FieldtypeMulti) {
$useEmpty = $isEmptyValue || $operator[0] === '<' || ((int) $value < 0 && $operator[0] === '>')
|| ($operator === '!=' && $isEmptyValue === false);
if($useEmpty && strpos($subfield, 'data') === 0) { // && !$fieldtype instanceof FieldtypeMulti) {
if($isEmptyValue) $numEmptyValues++;
if(in_array($operator, array('=', '!=', '<', '<=', '>', '>='))) {
// we only accommodate this optimization for single-value selectors...
@@ -1845,6 +1847,7 @@ class PageFinder extends Wire {
$q = $subqueries[$tableAlias];
} else {
$q = $this->wire(new DatabaseQuerySelect());
// $subqueries[$tableAlias] = $q;
}
/** @var PageFinderDatabaseQuerySelect $q */
@@ -1854,12 +1857,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";
@@ -1882,9 +1898,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))
@@ -1892,7 +1907,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 (";
@@ -1904,6 +1919,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
@@ -1972,6 +1989,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
*
@@ -1990,7 +2023,7 @@ class PageFinder extends Wire {
// what groups should be OR'd together
$sqls = array();
foreach($this->extraOrSelectors as $groupName => $selectorGroup) {
foreach($this->extraOrSelectors as /* $groupName => */ $selectorGroup) {
$n = 0;
$sql = "\tpages.id IN (\n";
foreach($selectorGroup as $selectors) {
@@ -2137,23 +2170,55 @@ class PageFinder extends Wire {
} else if($operator === '!=' || $operator === '<>') {
// not equals
// $whereType = 'AND';
if($value === "0" && !$ft->isEmptyValue($field, "0")) {
// may match rows with no value present
$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;
$blankValue = (string) $blankValue;
if($value === '') {
// match present rows that do not contain a blank string (or 0, when applicable)
$sql = "$tableAlias.$col IS NOT NULL AND ($tableAlias.$col!=''";
if($zeroIsEmpty) {
$sql .= " AND $tableAlias.$col!='0'";
} else {
$sql .= " OR $tableAlias.$col='0'";
}
$sql .= ')';
} else if($value === "0" && $zeroIsNotEmpty) {
// may match non-rows (no value present) or row with value=0
$sql = "$tableAlias.$col IS NULL OR $tableAlias.$col!='0'";
} else if($value !== "0" && $zeroIsEmpty) {
// match all rows except empty and those having specific non-empty value
$bindKey = $query->bindValueGetKey($value);
$sql = "$tableAlias.$col IS NULL OR $tableAlias.$col!=$bindKey";
} else if($blankIsObject) {
// match all present rows
$sql = "$tableAlias.$col IS NOT NULL";
} else {
$bindKey = $query->bindValueGetKey($blankValue);
$sql = "$tableAlias.$col IS NOT NULL AND ($tableAlias.$col!=$bindKey";
if($blankValue !== "0" && !$ft->isEmptyValue($field, "0")) {
// match all present rows that are not blankValue and not given blank value...
$bindKeyBlank = $query->bindValueGetKey($blankValue);
$bindKeyValue = $query->bindValueGetKey($value);
$sql = "$tableAlias.$col IS NOT NULL AND $tableAlias.$col!=$bindKeyValue AND ($tableAlias.$col!=$bindKeyBlank";
if($zeroIsNotEmpty && $blankValue !== "0" && $value !== "0") {
// ...allow for 0 to match also if 0 is not considered empty value
$sql .= " OR $tableAlias.$col='0'";
}
$sql .= ")";
}
if($ft instanceof FieldtypeMulti && !$ft->isEmptyValue($field, $value)) {
// when a multi-row field is in use, exclude match when any of the rows contain $value
$tableMulti = $table . "__multi$tableCnt";
$bindKey = $query->bindValueGetKey($value);
$query->leftjoin("$table AS $tableMulti ON $tableMulti.pages_id=pages.id AND $tableMulti.$col=$bindKey");
$query->where("$tableMulti.$col IS NULL");
}
} else if($operator == '<' || $operator == '<=') {
// less than
if($value > 0 && $ft->isEmptyValue($field, "0")) {
@@ -2243,6 +2308,7 @@ class PageFinder extends Wire {
if($user->isGuest()) {
// guest
foreach($this->templates as $template) {
/** @var Template $template */
if($template->guestSearchable || !$template->useRoles) {
$yesTemplates[$template->id] = $template;
continue;
@@ -2262,6 +2328,7 @@ class PageFinder extends Wire {
}
foreach($this->templates as $template) {
/** @var Template $template */
if($template->guestSearchable || !$template->useRoles) {
$yesTemplates[$template->id] = $template;
continue;
@@ -2276,6 +2343,7 @@ class PageFinder extends Wire {
// determine which templates the user is not allowed to access
foreach($this->templates as $template) {
/** @var Template $template */
if(!isset($yesTemplates[$template->id])) $noTemplates[$template->id] = $template;
}
@@ -2338,7 +2406,6 @@ class PageFinder extends Wire {
* @return string
*/
protected function ___getQueryAllowedTemplatesWhere(DatabaseQuerySelect $query, $where) {
if($query) {}
return $where;
}
@@ -2411,6 +2478,24 @@ class PageFinder extends Wire {
} else {
$value = "pages." . $database->escapeCol($value);
}
} else if(($value === 'path' || $value === 'url') && $this->wire()->modules->isInstalled('PagePaths')) {
static $pathN = 0;
$pathN++;
$pathsTable = "_sort_pages_paths$pathN";
if($language && !$language->isDefault() && $this->supportsLanguagePageNames()) {
$query->leftjoin("pages_paths AS $pathsTable ON $pathsTable.pages_id=pages.id AND $pathsTable.language_id=0");
$lid = (int) $language->id;
$asc = $descending ? 'DESC' : 'ASC';
$pathsLangTable = $pathsTable . "_$lid";
$s = "pages_paths AS $pathsLangTable ON $pathsLangTable.pages_id=pages.id AND $pathsLangTable.language_id=$lid";
$query->leftjoin($s);
$query->orderby("if($pathsLangTable.pages_id IS NULL, $pathsTable.path, $pathsLangTable.path) $asc");
$value = false;
} else {
$query->leftjoin("pages_paths AS $pathsTable ON $pathsTable.pages_id=pages.id");
$value = "$pathsTable.path";
}
} else {
// sort by custom field, or parent w/custom field
@@ -2460,7 +2545,7 @@ class PageFinder extends Wire {
$value = "COUNT($tableAlias.data)";
}
} else if(is_object($blankValue) && ($blankValue instanceof PageArray || $blankValue instanceof Page)) {
} else if($blankValue instanceof PageArray || $blankValue instanceof Page) {
// If it's a FieldtypePage, then data isn't worth sorting on because it just contains an ID to the page
// so we also join the page and sort on it's name instead of the field's "data" field.
if(!$subValue) $subValue = 'name';
@@ -2498,7 +2583,7 @@ class PageFinder extends Wire {
} else {
// regular field, just sort by data column
$value = "$tableAlias." . ($subValue ? $subValue : "data"); ;
$value = "$tableAlias." . ($subValue ? $subValue : "data");
}
}
@@ -2560,6 +2645,7 @@ class PageFinder extends Wire {
if($this->supportsLanguagePageNames()) {
$langNames = array();
foreach($this->languages as $language) {
/** @var Language $language */
if(!$language->isDefault()) $langNames[$language->id] = "name" . (int) $language->id;
}
if(!count($langNames)) $langNames = null;
@@ -2622,7 +2708,7 @@ class PageFinder extends Wire {
//$query->join("pages AS $alias ON ($lastAlias.parent_id=$alias.id AND $alias.name='$part')");
$bindKey = $query->bindValueGetKey($part);
$sql = "pages AS $alias ON ($lastAlias.parent_id=$alias.id AND ($alias.name=$bindKey";
if($langNames) foreach($langNames as $id => $name) {
if($langNames) foreach($langNames as /* $id => */ $name) {
// $status = "status" . (int) $id;
// $sql .= " OR ($alias.$name='$part' AND $alias.$status>0) ";
$bindKey = $query->bindValueGetKey($part);
@@ -2664,6 +2750,7 @@ class PageFinder extends Wire {
// the following fields are defined in each iteration here because they may be modified in the loop
$table = "pages";
$operator = $selector->operator;
$not = $selector->not;
$compareType = $selectors::getSelectorByOperator($operator, 'compareType');
$isPartialOperator = ($compareType & Selector::compareTypeFind);
@@ -2754,8 +2841,14 @@ class PageFinder extends Wire {
$field = $subfield;
}
}
} else if($field === 'id' && count($values) > 1 && $operator === '=' && !$selector->not) {
$IDs = $values;
} else if($field === 'id' && count($values) > 1) {
if($operator === '=') {
$IDs = $values;
} else if($operator === '!=' && !$not) {
$not = true;
$operator = '=';
$IDs = $values;
}
} else {
// primary field is not 'parent', 'children' or 'pages'
@@ -2763,10 +2856,10 @@ class PageFinder extends Wire {
if(count($IDs)) {
// parentIDs or IDs found via another query, and we don't need to match anything other than the parent ID
$in = $selector->not ? "NOT IN" : "IN";
$in = $not ? "NOT IN" : "IN";
$sql .= in_array($field, array('parent', 'parent_id')) ? "$table.parent_id " : "$table.id ";
$IDs = $sanitizer->intArray($IDs);
$strIDs = implode(',', $IDs);
$IDs = $sanitizer->intArray($IDs, array('strict' => true));
$strIDs = count($IDs) ? implode(',', $IDs) : '-1';
$sql .= "$in($strIDs)";
if($subfield === 'sort') $query->orderby("FIELD($table.id, $strIDs)");
unset($strIDs);
@@ -2788,7 +2881,7 @@ class PageFinder extends Wire {
} else if(in_array($field, array('created', 'modified', 'published'))) {
// prepare value for created, modified or published date fields
if(!ctype_digit($value)) {
if(!ctype_digit("$value")) {
$value = $datetime->strtotime($value);
}
if(empty($value)) {
@@ -2848,16 +2941,16 @@ class PageFinder extends Wire {
}
} else if(!$database->isOperator($operator)) {
$this->syntaxError("Operator '$operator' is not supported for '$field'.");
$s = '';
$this->syntaxError("Operator '$operator' is not supported for '$field'.");
} else if($this->isModifierField($field)) {
$this->syntaxError("Modifier '$field' is not allowed here");
$s = '';
$this->syntaxError("Modifier '$field' is not allowed here");
} else if(!$this->pagesColumnExists($field)) {
$this->syntaxError("Field '$field' is not a known field, column or selector modifier");
$s = '';
$this->syntaxError("Field '$field' is not a known field, column or selector modifier");
} else {
$not = false;
@@ -3181,7 +3274,7 @@ class PageFinder extends Wire {
/**
* Returns the templates ID, if it was part of the selector
*
* @return int
* @return int|null
*
*/
public function getTemplatesID() {
@@ -3223,12 +3316,11 @@ class PageFinder extends Wire {
protected function isPageField($fieldName, $literal = false) {
$is = false;
$field = null;
if($fieldName === 'parent' || $fieldName === 'children') {
return $fieldName; // early exit
} else if(is_object($fieldName) && $fieldName instanceof Field) {
} else if($fieldName instanceof Field) {
$field = $fieldName;
} else if(is_string($fieldName) && strpos($fieldName, '.')) {
@@ -3261,7 +3353,7 @@ class PageFinder extends Wire {
$is = $literal ? false : true;
} else {
$test = $field->type->getBlankValue(new NullPage(), $field);
if(is_object($test) && ($test instanceof Page || $test instanceof PageArray)) {
if($test instanceof Page || $test instanceof PageArray) {
$is = $literal ? false : true;
}
}
@@ -3333,6 +3425,7 @@ class PageFinder extends Wire {
self::$pagesColumns[$instanceID] = array();
if($this->supportsLanguagePageNames()) {
foreach($this->languages as $language) {
/** @var Language $language */
if($language->isDefault()) continue;
self::$pagesColumns[$instanceID]["name$language->id"] = true;
self::$pagesColumns[$instanceID]["status$language->id"] = true;
@@ -3420,16 +3513,11 @@ class PageFinder extends Wire {
);
$data = array_merge($_data, $data);
/** @var array $fields */
$fields = $data['fields'];
/** @var string $subfields */
$subfields = $data['subfields'];
/** @var Selector $selector */
$selector = $data['selector'];
/** @var DatabaseQuerySelect $query */
$query = $data['query'];
/** @var Wire|null $value */
$value = $this->wire($fieldName);
$fields = $data['fields']; /** @var array $fields */
$subfields = $data['subfields']; /** @var string $subfields */
$selector = $data['selector']; /** @var Selector $selector */
$query = $data['query']; /** @var DatabaseQuerySelect $query */
$value = $this->wire($fieldName); /** @var Wire|null $value */
if($value) {
// found an API var
@@ -3450,7 +3538,10 @@ class PageFinder extends Wire {
if($this->getQueryOwnerField($fieldName, $data)) return true;
return false;
/** @var bool|int|Field $value Hooks can modify return value to be Field */
$value = false;
return $value;
}
/**
@@ -3466,16 +3557,11 @@ class PageFinder extends Wire {
if(substr($fieldName, -7) !== '__owner') return false;
/** @var array $fields */
$fields = $data['fields'];
/** @var string $subfields */
$subfields = $data['subfields'];
/** @var Selectors $selectors */
$selectors = $data['selectors'];
/** @var Selector $selector */
$selector = $data['selector'];
/** @var DatabaseQuerySelect $query */
$query = $data['query'];
$fields = $data['fields']; /** @var array $fields */
$subfields = $data['subfields']; /** @var string $subfields */
$selectors = $data['selectors']; /** @var Selectors $selectors */
$selector = $data['selector']; /** @var Selector $selector */
$query = $data['query']; /** @var DatabaseQuerySelect $query */
if(empty($subfields)) $this->syntaxError("When using owner a subfield is required");
@@ -3493,6 +3579,7 @@ class PageFinder extends Wire {
// determine which templates are using $ownerFieldName
$templateIDs = array();
foreach($this->templates as $template) {
/** @var Template $template */
if($template->hasField($ownerFieldName)) {
$templateIDs[$template->id] = $template->id;
}
@@ -3524,10 +3611,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(
@@ -3599,7 +3687,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);
}
@@ -3690,6 +3778,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',
@@ -248,4 +250,4 @@ abstract class PageProperties {
if($remainder > 1) $names[$remainder] = "unknown-$remainder";
return $names;
}
}
}

View File

@@ -6,12 +6,13 @@
* Provides implementation for Page traversal functions.
* Based upon the jQuery traversal functions.
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
*/
class PageTraversal {
/**
* Return number of children, optionally with conditions
@@ -110,7 +111,7 @@ class PageTraversal {
* @param Page $page
* @param string|array $selector Selector to use, or blank to return all children
* @param array $options
* @return PageArray|array
* @return PageArray
*
*/
public function children(Page $page, $selector = '', $options = array()) {
@@ -214,6 +215,7 @@ class PageTraversal {
$stop = false;
foreach($parents->reverse() as $parent) {
/** @var Page $parent */
if(is_string($selector) && strlen($selector)) {
if(ctype_digit("$selector") && $parent->id == $selector) {
@@ -431,7 +433,7 @@ class PageTraversal {
'parent_id' => $parent->id,
'cache' => $page->loaderCache
));
if($options['all'] && $options['prev']) $result = $result->reverse();
if($options['prev']) $result = $result->reverse();
} else {
$row = reset($rows);
@@ -622,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,
);
@@ -648,13 +650,14 @@ class PageTraversal {
$options['pageNum'] = $input->pageNum();
}
if(count($options['urlSegments'])) {
if(is_array($options['urlSegments']) && count($options['urlSegments'])) {
$str = '';
if(is_string($options['urlSegments'][0])) {
reset($options['urlSegments']);
if(is_string(key($options['urlSegments']))) {
// associative array converts to key/value style URL segments
foreach($options['urlSegments'] as $key => $value) {
$str .= "$key/$value/";
if(is_int($key)) $str = '';
if(is_int($key)) $str = ''; // abort assoc array option if any int key found
if($str === '') break;
}
}
@@ -704,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 .= '/';
}
}
@@ -717,7 +720,7 @@ class PageTraversal {
if($options['scheme']) {
$scheme = strtolower($options['scheme']);
if(strpos($scheme, '://') === false) $scheme .= '://';
if($scheme === 'https://' && $config->noHTTPS) $scheme = 'http://';
if($scheme === 'https://' && $config->noHTTPS) $scheme = 'http' . '://';
$host = $options['host'] ? $options['host'] : $config->httpHost;
$url = "$scheme$host$url";
@@ -768,7 +771,6 @@ class PageTraversal {
'language' => null,
);
/** @var Modules $modules */
$modules = $page->wire()->modules;
$options = array_merge($defaults, $options);
$languages = $options['languages'] ? $page->wire()->languages : null;
@@ -788,6 +790,7 @@ class PageTraversal {
// include other language URLs
if($languages && $languages->hasPageNames()) {
foreach($languages as $language) {
/** @var Language $language */
if(!$language->isDefault() && !$page->get("status$language")) continue;
$urls[$language->name] = $page->localUrl($language);
}
@@ -849,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
*
*/
@@ -859,25 +863,33 @@ 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) {
$url = ($https || $config->https ? 'https://' : 'http://') . $config->httpHost . $url;
$url = ($https || $config->https ? 'https' : 'http' ) . '://' . $config->httpHost . $url;
}
}
$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');
@@ -1001,7 +1013,7 @@ class PageTraversal {
* Return pages that this page is referencing by way of Page reference fields
*
* @param Page $page
* @param bool $field Limit results to requested field, or specify boolean true to return array indexed by field names.
* @param bool|Field|string|int $field Limit results to requested field, or specify boolean true to return array indexed by field names.
* @param bool $getCount Specify true to return count(s) rather than pages.
* @return PageArray|int|array
*
@@ -1027,6 +1039,7 @@ class PageTraversal {
$itemsByField = array();
foreach($page->template->fieldgroup as $f) {
/** @var Field $f */
if($fieldName && $field->name != $fieldName) continue;
if(!$f->type instanceof FieldtypePage) continue;
if($byField) $itemsByField[$f->name] = $pages->newPageArray();
@@ -1134,12 +1147,12 @@ 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) {
if(is_object($selector) && $selector instanceof PageArray) {
public function nextSibling(Page $page, $selector = '', ?PageArray $siblings = null) {
if($selector instanceof PageArray) {
// backwards compatible to when $siblings was first argument
$siblings = $selector;
$selector = '';
@@ -1155,7 +1168,7 @@ class PageTraversal {
/** @var Page $next */
$next = $siblings->getNext($next, false);
if(empty($selector) || !$next || $next->matches($selector)) break;
} while($next && $next->id);
} while($next->id);
if(is_null($next)) $next = $page->wire()->pages->newNullPage();
return $next;
}
@@ -1178,12 +1191,12 @@ 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) {
if(is_object($selector) && $selector instanceof PageArray) {
public function prevSibling(Page $page, $selector = '', ?PageArray $siblings = null) {
if($selector instanceof PageArray) {
// backwards compatible to when $siblings was first argument
$siblings = $selector;
$selector = '';
@@ -1199,7 +1212,7 @@ class PageTraversal {
/** @var Page $prev */
$prev = $siblings->getPrev($prev, false);
if(empty($selector) || !$prev || $prev->matches($selector)) break;
} while($prev && $prev->id);
} while($prev->id);
if(is_null($prev)) $prev = $page->wire()->pages->newNullPage();
return $prev;
}
@@ -1209,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();
@@ -1243,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();
@@ -1278,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();
@@ -1291,6 +1304,7 @@ class PageTraversal {
$stop = false;
foreach($siblings as $sibling) {
/** @var Page $sibling */
if(is_string($selector) && strlen($selector)) {
if(ctype_digit("$selector") && $sibling->id == $selector) {
@@ -1329,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();
@@ -1342,6 +1356,7 @@ class PageTraversal {
$stop = false;
foreach($siblings->reverse() as $sibling) {
/** @var Page $sibling */
if(is_string($selector) && strlen($selector)) {
if(ctype_digit("$selector") && $sibling->id == $selector) {

1132
wire/core/PageValues.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@
* Pagefile objects are contained by a `Pagefiles` object.
* #pw-body
*
* ProcessWire 3.x, Copyright 2020 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.
@@ -44,6 +44,8 @@
* @property int $modified_users_id ID of user that last modified the file or 0 if not known (3.0.154+). #pw-group-other
* @property User|NullPage $createdUser User that added/uploaded the file or NullPage if not known (3.0.154)+. #pw-group-other
* @property User|NullPage $modifiedUser User that last modified the file or NullPage if not known (3.0.154)+. #pw-group-other
* @property bool $formatted True when value has had Textformatters applied. #pw-internal
* @property string $uploadName Original unsanitized filename at upload, see notes for uploadName() method (3.0.212+). #pw-group-other
*
* @method void install($filename)
* @method string httpUrl()
@@ -51,7 +53,7 @@
*
*/
class Pagefile extends WireData {
class Pagefile extends WireData implements WireArrayItem {
/**
* Timestamp 'created' used by pagefiles that are temporary, not yet published
@@ -132,6 +134,7 @@ class Pagefile extends WireData {
$this->pagefiles = $pagefiles;
if(strlen($filename)) $this->setFilename($filename);
$this->set('description', '');
$this->set('tags', '');
$this->set('formatted', false); // has an output formatter been run on this Pagefile?
@@ -140,6 +143,8 @@ class Pagefile extends WireData {
$this->set('filesize', 0);
$this->set('created_users_id', 0);
$this->set('modified_users_id', 0);
parent::__construct();
}
/**
@@ -230,13 +235,14 @@ class Pagefile extends WireData {
if(!is_readable($filename)) throw new WireException("Unable to read: $filename");
if(!copy($filename, $destination)) throw new WireException("Unable to copy: $filename => $destination");
} else {
/** @var WireHttp $http */
$http = $this->wire(new WireHttp());
// note: download() method throws excepton on failure
$http->download($filename, $destination);
// download was successful
}
$this->wire('files')->chmod($destination);
$this->wire()->files->chmod($destination);
$this->changed('file');
$this->isNew(true);
parent::set('basename', $basename);
@@ -286,7 +292,7 @@ class Pagefile extends WireData {
if(strpos($key, 'description') === 0 && preg_match('/^description(\d+)$/', $value, $matches)) {
// check if a language description is being set manually by description123 where 123 is language ID
$languages = $this->wire('languages');
$languages = $this->wire()->languages;
if($languages) {
$language = $languages->get((int) $matches[1]);
if($language && $language->id) return $this->setDescription($value, $language);
@@ -310,10 +316,10 @@ class Pagefile extends WireData {
*/
protected function setUser($user, $type) {
$id = 0;
if($user === true) $user = $this->wire('user');
if($user === true) $user = $this->wire()->user;
if(is_object($user)) {
if($user instanceof NullPage) {
$id = 0;
// $id = 0;
} else if($user instanceof User) {
$id = $user->isGuest() ? 0 : $user->id;
}
@@ -322,8 +328,8 @@ class Pagefile extends WireData {
} else if(ctype_digit($user)) {
$id = (int) $user;
} else if(is_string($user)) {
$name = $this->wire('sanitizer')->pageName($user);
$user = $name ? $this->wire('users')->get("name=$name") : null;
$name = $this->wire()->sanitizer->pageName($user);
$user = $name ? $this->wire()->users->get("name=$name") : null;
$id = $user && $user->id ? $user->id : 0;
}
if($id < 0) $id = 0;
@@ -351,7 +357,7 @@ class Pagefile extends WireData {
$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;
}
@@ -415,14 +421,13 @@ class Pagefile extends WireData {
* 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) {
/** @var Languages $languages */
$languages = $this->wire('languages');
$languages = $this->wire()->languages;
/** @var Language|null $language */
@@ -467,7 +472,7 @@ class Pagefile extends WireData {
if(!$id) $id = '';
$name = $n > 0 ? "description$id" : "description";
} else if($id === 'default') {
$name = 'description';
// $name = 'description';
} else if($languages) {
$language = $languages->get($id); // i.e. "default" or "es"
if(!$language->id) continue;
@@ -478,8 +483,8 @@ class Pagefile extends WireData {
}
} else {
// no JSON values so assume regular language description
$languages = $this->wire('languages');
$language = $languages ? $this->wire('user')->language : null;
$languages = $this->wire()->languages;
$language = $languages ? $this->wire()->user->language : null;
if($languages && $language && !$noLang && !$language->isDefault()) {
$name = "description$language->id";
@@ -533,13 +538,14 @@ class Pagefile extends WireData {
*/
public function description($language = null, $value = null) {
$languages = $this->wire()->languages;
if($language === true && $value === true) {
// return all in array indexed by language name
/** @var Languages $languages */
$languages = $this->wire('languages');
if(!$languages) return array('default' => parent::get('description'));
$value = array();
foreach($languages as $language) {
/** @var Language $language */
$value[$language->name] = (string) parent::get("description" . ($language->isDefault() ? '' : $language->id));
}
return $value;
@@ -564,16 +570,18 @@ class Pagefile extends WireData {
$value = null;
}
if((is_string($language) || is_int($language)) && $this->wire('languages')) {
if((is_string($language) || is_int($language)) && $languages) {
// convert named or ID'd languages to Language object
$language = $this->wire('languages')->get($language);
$language = $languages->get($language);
}
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}");
if($user->language && $user->language->id) {
$value = parent::get("description{$user->language}");
}
if(empty($value)) {
// inherit default language value
$value = parent::get("description");
@@ -581,10 +589,10 @@ class Pagefile extends WireData {
} else if($language === true) {
// return JSON string of all languages if applicable
$languages = $this->wire('languages');
if($languages && $languages->count() > 1) {
$values = array(0 => parent::get("description"));
foreach($languages as $lang) {
/** @var Language $lang */
if($lang->isDefault()) continue;
$v = parent::get("description$lang");
if(empty($v)) continue;
@@ -600,8 +608,11 @@ class Pagefile extends WireData {
} else if(is_object($language) && $language->id) {
// return description for specific language or blank if not available
if($language->isDefault()) $value = parent::get("description");
else $value = parent::get("description$language");
if($language->isDefault()) {
$value = parent::get("description");
} else {
$value = parent::get("description$language");
}
}
// we only return strings, so return blank rather than null
@@ -620,10 +631,9 @@ class Pagefile extends WireData {
*
*/
public function get($key) {
$value = null;
if($key == 'name') $key = 'basename';
if($key == 'pathname') $key = 'filename';
if($key === 'name') $key = 'basename';
if($key === 'pathname') $key = 'filename';
switch($key) {
case 'url':
@@ -668,7 +678,7 @@ class Pagefile extends WireData {
case 'modifiedStr':
case 'createdStr':
$value = parent::get(str_replace('Str', '', $key));
$value = wireDate($this->wire('config')->dateFormat, $value);
$value = wireDate($this->wire()->config->dateFormat, $value);
break;
case 'created_users_id':
case 'modified_users_id':
@@ -688,16 +698,21 @@ class Pagefile extends WireData {
break;
case 'mtimeStr':
case 'filemtimeStr':
$value = wireDate($this->wire('config')->dateFormat, $this->filemtime());
$value = wireDate($this->wire()->config->dateFormat, $this->filemtime());
break;
case 'fieldValues':
return $this->fieldValues;
$value = $this->fieldValues;
break;
case 'uploadName':
$value = $this->uploadName();
break;
default:
if(strpos($key, '|')) return parent::get($key);
$value = $this->getFieldValue($key);
}
if(is_null($value)) return parent::get($key);
return $value;
}
@@ -714,7 +729,7 @@ class Pagefile extends WireData {
*/
public function getFieldValue($name, $formatted = null) {
$field = $this->wire('fields')->get($name);
$field = $this->wire()->fields->get($name);
if(!$field) return null;
$template = $this->pagefiles->getFieldsTemplate();
@@ -787,7 +802,7 @@ class Pagefile extends WireData {
if($changed === null && $this->page->trackChanges()) {
// detect if a change has taken place
$oldValue = $this->getFieldValue($field->name, false);
if(is_object($oldValue) && $oldValue instanceof Wire && $oldValue === $value) {
if($oldValue instanceof Wire && $oldValue === $value) {
// $oldValue and new $value are the same object instance, so ask it if anything has changed
$changed = $oldValue->isChanged();
if($changed) $this->trackChange($field->name);
@@ -822,11 +837,13 @@ class Pagefile extends WireData {
*
* #pw-group-traversal
*
* @return Pagefile|Wire|null
* @return Pagefile|null
*
*/
public function getNext() {
return $this->pagefiles->getNext($this);
/** @var Pagefile|null $item */
$item = $this->pagefiles->getNext($this);
return $item;
}
/**
@@ -834,11 +851,13 @@ class Pagefile extends WireData {
*
* #pw-group-traversal
*
* @return Pagefile|Wire|null
* @return Pagefile|null
*
*/
public function getPrev() {
return $this->pagefiles->getPrev($this);
/** @var Pagefile|null $item */
$item = $this->pagefiles->getPrev($this);
return $item;
}
/**
@@ -859,7 +878,7 @@ class Pagefile extends WireData {
*
*/
public function url() {
return $this->wire('hooks')->isHooked('Pagefile::url()') ? $this->__call('url', array()) : $this->___url();
return $this->wire()->hooks->isHooked('Pagefile::url()') ? $this->__call('url', array()) : $this->___url();
}
/**
@@ -895,7 +914,7 @@ class Pagefile extends WireData {
*
*/
public function filename() {
return $this->wire('hooks')->isHooked('Pagefile::filename()') ? $this->__call('filename', array()) : $this->___filename();
return $this->wire()->hooks->isHooked('Pagefile::filename()') ? $this->__call('filename', array()) : $this->___filename();
}
/**
@@ -919,6 +938,24 @@ class Pagefile extends WireData {
return $basename;
}
/**
* Original and unsanitized filename at the time it was uploaded
*
* Returned value is also entity encoded if $pages output formatting state is ON.
* For files uploaded in ProcessWire 3.0.212 or newer. Falls back to current file
* basename for files that were uploaded prior to 3.0.212.
*
* @return string
* @since 3.0.212
*
*/
public function uploadName() {
$uploadName = (string) $this->filedata('uploadName');
if(!strlen($uploadName)) $uploadName = $this->basename();
if($this->page && $this->page->of()) $uploadName = $this->wire()->sanitizer->entities($uploadName);
return $uploadName;
}
/**
* Get or set the "tags" property, when in use.
*
@@ -950,7 +987,7 @@ class Pagefile extends WireData {
$tags = str_replace(array(',', '|'), ' ', $tags);
$_tags = explode(' ', $tags);
$tags = array();
foreach($_tags as $key => $tag) {
foreach($_tags as /* $key => */ $tag) {
$tag = trim($tag);
if($value === false) $tag = strtolower($tag); // force lowercase
if(!strlen($tag)) continue;
@@ -959,7 +996,7 @@ class Pagefile extends WireData {
} else if($value !== null) {
// set tags
if(is_array($value)) $value = implode(' ', $value); // convert to string
$value = $this->wire('sanitizer')->text($value);
$value = $this->wire()->sanitizer->text($value);
if(strpos($value, "\t") !== false) $value = str_replace("\t", " ", $value);
// collapse extra whitespace
while(strpos($value, " ") !== false) $value = str_replace(" ", " ", $value);
@@ -1074,6 +1111,7 @@ class Pagefile extends WireData {
*
*/
public function addTag($tag) {
$sanitizer = $this->wire()->sanitizer;
if(is_array($tag)) {
$addTags = $tag;
} else if(strpos($tag, ',') !== false) {
@@ -1085,7 +1123,7 @@ class Pagefile extends WireData {
$numAdded = 0;
foreach($addTags as $tag) {
if($this->hasTag($tag)) continue;
$tag = $this->wire('sanitizer')->text(trim($tag));
$tag = $sanitizer->text(trim($tag));
$tag = str_replace(' ', '_', $tag);
$tags[strtolower($tag)] = $tag;
$numAdded++;
@@ -1227,11 +1265,10 @@ class Pagefile extends WireData {
public function unlink() {
/** @var WireFileTools $files */
if(!strlen($this->basename) || !is_file($this->filename)) return true;
$files = $this->wire('files');
foreach($this->extras() as $extra) {
$extra->unlink();
}
return $files->unlink($this->filename, true);
return $this->wire()->files->unlink($this->filename, true);
}
/**
@@ -1250,7 +1287,7 @@ class Pagefile extends WireData {
$extra->filename(); // init
}
$basename = $this->pagefiles->cleanBasename($basename, true);
if($this->wire('files')->rename($this->filename, $this->pagefiles->path . $basename, true)) {
if($this->wire()->files->rename($this->filename, $this->pagefiles->path . $basename, true)) {
$this->set('basename', $basename);
$basename = $this->basename();
foreach($this->extras() as $extra) {
@@ -1271,8 +1308,7 @@ class Pagefile extends WireData {
*
*/
public function copyToPath($path) {
/** @var WireFileTools $files */
$files = $this->wire('files');
$files = $this->wire()->files;
$result = $files->copy($this->filename(), $path);
foreach($this->extras() as $extra) {
if(!$extra->exists()) continue;
@@ -1351,14 +1387,14 @@ class Pagefile extends WireData {
* #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 !== null && $value instanceof PagefileExtra) {
if($value instanceof PagefileExtra) {
$this->extras[$name] = $value;
}
return isset($this->extras[$name]) ? $this->extras[$name] : null;
@@ -1394,8 +1430,8 @@ class Pagefile extends WireData {
*/
public function replaceFile($filename, $move = true) {
/** @var WireFileTools $files */
$files = $this->wire('files');
$files = $this->wire()->files;
if(!is_file($filename) || !is_readable($filename)) return false;
if($move && !is_writable($filename)) $move = false;
@@ -1424,6 +1460,45 @@ class Pagefile extends WireData {
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
@@ -1437,6 +1512,19 @@ class Pagefile extends WireData {
return $this->get($key) !== null;
}
/**
* For WireArrayItem interface
*
* #pw-internal
*
* @return Pagefiles
* @since 3.0.205
*
*/
public function getWireArray() {
return $this->pagefiles;
}
/**
* Debug info
*
@@ -1463,4 +1551,3 @@ class Pagefile extends WireData {
return $info;
}
}

View File

@@ -224,7 +224,7 @@ class PagefileExtra extends WireData {
*/
public function unlink() {
if(!$this->exists()) return false;
return $this->wire('files')->unlink($this->filename());
return $this->wire()->files->unlink($this->filename());
}
/**
@@ -235,7 +235,7 @@ class PagefileExtra extends WireData {
*/
public function rename() {
if(!$this->filenamePrevious || !is_readable($this->filenamePrevious)) return false;
return $this->wire('files')->rename($this->filenamePrevious, $this->filename());
return $this->wire()->files->rename($this->filenamePrevious, $this->filename());
}
/**
@@ -322,4 +322,4 @@ class PagefileExtra extends WireData {
public function __toString() {
return $this->basename();
}
}
}

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;
}
/**
@@ -193,11 +195,12 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
*
* #pw-internal
*
* @return Pagefiles|WireArray
* @return Pagefiles|Pageimages|WireArray
*
*/
public function makeNew() {
$class = get_class($this);
/** @var Pagefiles|Pageimages $newArray */
$newArray = $this->wire(new $class($this->page));
$newArray->setField($this->field);
return $newArray;
@@ -220,7 +223,10 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
foreach($this->data as $key => $value) $newArray[$key] = $value;
foreach($this->extraData as $key => $value) $newArray->data($key, $value);
$newArray->resetTrackChanges($this->trackChanges());
foreach($newArray as $item) $item->setPagefilesParent($newArray);
foreach($newArray as $item) {
/** @var Pagefile $item */
$item->setPagefilesParent($newArray);
}
return $newArray;
}
@@ -232,6 +238,7 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
*/
public function __clone() {
foreach($this as $key => $pagefile) {
/** @var Pagefile $pagefile */
$pagefile = clone $pagefile;
$pagefile->setPagefilesParent($this);
$this->set($key, $pagefile);
@@ -299,13 +306,13 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
/**
* Get for direct access to properties
*
* @param int|string $key
* @param int|string $name
* @return bool|mixed|Page|Wire|WireData
*
*/
public function __get($key) {
if(in_array($key, array('page', 'field', 'url', 'path'))) return $this->get($key);
return parent::__get($key);
public function __get($name) {
if(in_array($name, array('page', 'field', 'url', 'path'))) return $this->get($name);
return parent::__get($name);
}
/**
@@ -358,7 +365,6 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
}
}
/** @var Pagefiles $result */
$result = parent::add($item);
return $result;
}
@@ -371,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();
@@ -427,12 +435,13 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
*
* #pw-internal Please use the hookable delete() method for public API
*
* @param Pagefile $item Item to delete/remove.
* @param Pagefile $key Item to delete/remove.
* @return $this
* @throws WireException
*
*/
public function remove($item) {
public function remove($key) {
$item = $key;
if(is_string($item)) $item = $this->get($item);
if(!$this->isValidItem($item)) throw new WireException("Invalid type to {$this->className}::remove(item)");
$this->addSaveHook();
@@ -519,8 +528,7 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
$pathname = $n ? ($path . $parts[0] . "-$n." . $parts[1]) : ($path . $item->basename);
} while(file_exists($pathname) && $n++);
if(copy($item->filename(), $pathname)) {
$this->wire('files')->chmod($pathname);
if($this->wire()->files->copy($item->filename(), $pathname)) {
$itemCopy = clone $item;
$itemCopy->setPagefilesParent($pagefiles);
@@ -591,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;
@@ -630,6 +639,7 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
public function findTag($tag) {
$items = $this->makeNew();
foreach($this as $pagefile) {
/** @var Pagefile $pagefile */
if($pagefile->hasTag($tag)) $items->add($pagefile);
}
return $items;
@@ -656,6 +666,7 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
public function getTag($tag) {
$item = null;
foreach($this as $pagefile) {
/** @var Pagefile $pagefile */
if(!$pagefile->hasTag($tag)) continue;
$item = $pagefile;
break;
@@ -706,6 +717,7 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
// return array of tags
$tags = array();
foreach($this as $pagefile) {
/** @var Pagefile $pagefile */
$tags = array_merge($tags, $pagefile->tags($value));
}
if($returnString) $tags = implode(' ', $tags);
@@ -729,7 +741,6 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
*/
public function trackChange($what, $old = null, $new = null) {
if($this->field && $this->page) $this->page->trackChange($this->field->name);
/** @var Pagefiles $result */
$result = parent::trackChange($what, $old, $new);
return $result;
}
@@ -745,6 +756,7 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
$hasFile = null;
$name = basename($name);
foreach($this as $pagefile) {
/** @var Pagefile $pagefile */
if($pagefile->basename == $name) {
$hasFile = $pagefile;
break;
@@ -773,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}";
@@ -799,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);
}
}
}
@@ -853,13 +869,18 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
public function deleteAllTemp() {
$removed = array();
foreach($this as $pagefile) {
/** @var Pagefile $pagefile */
if(!$this->isTemp($pagefile, 'deletable')) continue;
$removed[] = $pagefile->basename();
$this->remove($pagefile);
}
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);
}
@@ -950,7 +971,7 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
$this->fieldsTemplate = false;
/** @var FieldtypeFile $fieldtype */
$fieldtype = $field->type;
$template = $fieldtype && $fieldtype instanceof FieldtypeFile ? $fieldtype->getFieldsTemplate($field) : null;
$template = $fieldtype instanceof FieldtypeFile ? $fieldtype->getFieldsTemplate($field) : null;
if($template) $this->fieldsTemplate = $template;
}
}
@@ -971,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

@@ -101,6 +101,7 @@ class PagefilesManager extends Wire {
*
*/
public function __construct(Page $page) {
parent::__construct();
$page->wire($this);
$this->init($page);
}
@@ -166,13 +167,14 @@ class PagefilesManager extends Wire {
if($pageID == $this->page->id) {
$page = $this->page;
} else {
$page = $this->wire('pages')->get($pageID);
$page = $this->wire()->pages->get($pageID);
}
if(!$page->id) return null;
} else {
$page = $this->page;
}
foreach($page->fieldgroup as $field) {
/** @var Field $field */
if(!$field->type instanceof FieldtypeFile) continue;
$pagefiles = $page->get($field->name);
// if mapping to single file, ask it for the parent array
@@ -303,7 +305,7 @@ class PagefilesManager extends Wire {
protected function _createPath($path) {
if(empty($path)) return false;
if(is_dir("$path")) return true;
return $this->wire('files')->mkdir($path, true);
return $this->wire()->files->mkdir($path, true);
}
/**
@@ -329,21 +331,22 @@ class PagefilesManager extends Wire {
*
*/
public function emptyPath($rmdir = false, $recursive = true) {
$files = $this->wire()->files;
$path = $this->path();
if(!is_dir($path)) return true;
$errors = 0;
if($recursive) {
// clear out path and everything below it
if(!$this->wire('files')->rmdir($path, true, true)) $errors++;
if(!$files->rmdir($path, true, true)) $errors++;
if(!$rmdir) $this->_createPath($path);
} else {
// only clear out files in path
foreach(new \DirectoryIterator($path) as $file) {
if($file->isDot() || $file->isDir()) continue;
if(!$this->wire('files')->unlink($file->getPathname(), true)) $errors++;
if(!$files->unlink($file->getPathname(), true)) $errors++;
}
if($rmdir) {
$this->wire('files')->rmdir($path, false, true); // will not be successful if other dirs within it
$files->rmdir($path, false, true); // will not be successful if other dirs within it
}
}
return $errors === 0;
@@ -375,7 +378,7 @@ class PagefilesManager extends Wire {
*
*/
public function path() {
return $this->wire('hooks')->isHooked('PagefilesManager::path()') ? $this->__call('path', array()) : $this->___path();
return $this->wire()->hooks->isHooked('PagefilesManager::path()') ? $this->__call('path', array()) : $this->___path();
}
/**
@@ -405,7 +408,7 @@ class PagefilesManager extends Wire {
*
*/
public function url() {
return $this->wire('hooks')->isHooked('PagefilesManager::url()') ? $this->__call('url', array()) : $this->___url();
return $this->wire()->hooks->isHooked('PagefilesManager::url()') ? $this->__call('url', array()) : $this->___url();
}
/**
@@ -421,10 +424,11 @@ class PagefilesManager extends Wire {
*/
public function ___url() {
if(!is_null($this->url)) return $this->url;
if(strpos($this->path(), $this->config->paths->files . self::extendedDirName) !== false) {
$this->url = $this->config->urls->files . self::_dirExtended($this->page->id);
$config = $this->wire()->config;
if(strpos($this->path(), $config->paths->files . self::extendedDirName) !== false) {
$this->url = $config->urls->files . self::_dirExtended($this->page->id);
} else {
$this->url = $this->config->urls->files . $this->page->id . '/';
$this->url = $config->urls->files . $this->page->id . '/';
}
return $this->url;
}
@@ -457,15 +461,15 @@ class PagefilesManager extends Wire {
/**
* Handle non-function versions of some properties
*
* @param string $key
* @param string $name
* @return mixed
*
*/
public function __get($key) {
if($key == 'path') return $this->path();
if($key == 'url') return $this->url();
if($key == 'page') return $this->page;
return parent::__get($key);
public function __get($name) {
if($name === 'path') return $this->path();
if($name === 'url') return $this->url();
if($name === 'page') return $this->page;
return parent::__get($name);
}
/**
@@ -555,7 +559,7 @@ class PagefilesManager extends Wire {
if($secureFiles === false) {
// use the public path, renaming a secure path to public if it exists
if(is_dir($securePath) && !is_dir($publicPath) && $secureFiles !== null) {
if(is_dir($securePath) && !is_dir($publicPath)) {
$page->wire()->files->rename($securePath, $publicPath);
self::$numRenamedPaths++;
}
@@ -596,6 +600,76 @@ class PagefilesManager extends Wire {
return $filesPath;
}
/**
* Get all potential disk paths for given Page files (not yet in use)
*
* @todo FOR FUTURE USE
* @param Page $page
* @return string[]
*
static public function _paths(Page $page) {
$config = $page->wire()->config;
$path = $config->paths->files;
$securePrefix = $config->pagefileSecurePathPrefix;
$useSecure = $page->secureFiles();
$useExtended = $config->pagefileExtendedPaths;
$useUnique = $config->pagefileUnique && $page->hasStatus(Page::statusUnique);
if(!strlen($securePrefix)) $securePrefix = self::defaultSecurePathPrefix;
$paths = array(
'current' => '',
'normal' => $path . "$page->id/",
'unique' => $path . "0/$page->name/",
'extended' => $path . self::_dirExtended($page->id),
'secure' => $path . $securePrefix . "$page->id/",
'secureUnique' => $path . "0/$securePrefix$page->name/",
'secureExtended' => $path . self::_dirExtended($page->id, $securePrefix),
);
if($useUnique) {
// use unique page name paths
$paths['current'] = ($useSecure ? $paths['secureUnique'] : $paths['unique']);
} else if($useSecure) {
// use secure files
$paths['current'] = ($useExtended ? $paths['secureExtended'] : $paths['secure']);
} else {
// use normal path
$paths['current'] = ($useExtended ? $paths['extended'] : $paths['normal']);
}
return $paths;
}
*/
/**
* Scan all paths for page and make sure only the correct one exists (not yet in use)
*
* Also crates and moves files when necessary.
*
* @todo FOR FUTURE USE
* @param Page $page
*
private function verifyPaths(Page $page) {
$paths = self::_paths($page);
$current = $paths['current'];
$currentExists = is_dir($current);
unset($paths['current']);
foreach($paths as $path) {
if(!is_dir($path)) continue;
if(!$currentExists) {
$this->_createPath($current);
$currentExists = true;
}
$this->_copyFiles($path, $current);
$this->wire()->files->rmdir($path, true);
self::$numRenamedPaths++;
}
}
*/
/**
* Get quantity of renamed paths to to pagefileSecure changes
*
@@ -656,7 +730,7 @@ class PagefilesManager extends Wire {
$parts = explode('/', $dir);
$pageID = '';
$securePrefix = wire('config')->pagefileSecurePathPrefix;
$securePrefix = wire()->config->pagefileSecurePathPrefix;
if(!strlen($securePrefix)) $securePrefix = self::defaultSecurePathPrefix;
foreach(array_reverse($parts) as $key => $part) {
@@ -684,7 +758,7 @@ class PagefilesManager extends Wire {
$this->wire($wtd);
$wtd->setMaxAge(3600);
$name = $wtd->createName('PFM');
$wtd->create($name);
$wtd->init($name);
}
return $wtd->get();
// if(is_null($wtd)) $wtd = $this->wire(new WireTempDir($this->className() . $this->page->id));

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
*
@@ -190,7 +190,8 @@ class Pageimage extends Pagefile {
*
*/
public function url() {
if($this->wire('hooks')->isHooked('Pagefile::url()') || $this->wire('hooks')->isHooked('Pageimage::url()')) {
$hooks = $this->wire()->hooks;
if($hooks->isHooked('Pagefile::url()') || $hooks->isHooked('Pageimage::url()')) {
return $this->__call('url', array());
} else {
return $this->___url();
@@ -206,7 +207,8 @@ class Pageimage extends Pagefile {
*
*/
public function filename() {
if($this->wire('hooks')->isHooked('Pagefile::filename()') || $this->wire('hooks')->isHooked('Pageimage::filename()')) {
$hooks = $this->wire()->hooks;
if($hooks->isHooked('Pagefile::filename()') || $hooks->isHooked('Pageimage::filename()')) {
return $this->__call('filename', array());
} else {
return $this->___filename();
@@ -327,7 +329,7 @@ class Pageimage extends Pagefile {
// UNSET
$this->filedata(false, 'focus');
} else if($top !== null && $left !== null) {
} else if($left !== null) {
// SET
if(is_array($top)) {
if(isset($top['left'])) {
@@ -487,12 +489,13 @@ class Pageimage extends Pagefile {
$imageInfo = $this->imageInfo;
$filename = is_string($reset) && file_exists($reset) ? $reset : '';
$ext = $this->ext;
if(!$reset && $imageInfo['width'] && !$filename) {
return $imageInfo;
}
if($this->ext == 'svg') {
if($ext == 'svg') {
$imageInfo = array_merge($imageInfo, $this->getImageInfoSVG($filename));
} else {
if($filename) {
@@ -507,6 +510,14 @@ class Pageimage extends Pagefile {
} else if($info) {
$imageInfo['width'] = $info[0];
$imageInfo['height'] = $info[1];
if(function_exists('exif_read_data') && ($ext === 'jpg' || $ext === 'jpeg')) {
$exif = $filename ? @exif_read_data($filename) : @exif_read_data($this->filename);
if(!empty($exif['Orientation']) && (int) $exif['Orientation'] > 4) {
// Image has portrait orientation so reverse width and height info
$imageInfo['width'] = $info[1];
$imageInfo['height'] = $info[0];
}
}
}
}
@@ -532,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'))) {
@@ -605,7 +623,7 @@ class Pageimage extends Pagefile {
*
* - `quality` (int): Quality setting 1-100 (default=90, or as specified in /site/config.php).
* - `upscaling` (bool): Allow image to be upscaled? (default=true).
* - `cropping` (string|bool|array): Cropping mode, see possible values in "cropping" section below (default=center).
* - `cropping` (string|bool|array): Cropping mode, see possible values in "cropping" section below (default=true).
* - `suffix` (string|array): Suffix word to identify the new image, or use array of words for multiple (default=none).
* - `forceNew` (bool): Force re-creation of the image even if it already exists? (default=false).
* - `sharpening` (string): Sharpening mode: "none", "soft", "medium", or "strong" (default=soft).
@@ -679,7 +697,7 @@ class Pageimage extends Pagefile {
return $this->sizeName($width, $options);
}
if($this->wire('hooks')->isHooked('Pageimage::size()')) {
if($this->wire()->hooks->isHooked('Pageimage::size()')) {
$result = $this->__call('size', array($width, $height, $options));
} else {
$result = $this->___size($width, $height, $options);
@@ -710,7 +728,7 @@ class Pageimage extends Pagefile {
protected function ___size($width, $height, $options) {
$this->error = '';
if($this->ext == 'svg') return $this;
if($this->ext === 'svg') return $this;
if(!is_array($options)) $options = $this->sizeOptionsToArray($options);
// originally requested options
@@ -739,16 +757,16 @@ class Pageimage extends Pagefile {
'focus' => true, // allow single dimension resizes to use focus area?
'zoom' => null, // zoom override, used only if focus is applicable, int when populated
'allowOriginal' => false, // Return original image if already at requested dimensions? (must be only specified option)
);
);
/** @var WireFileTools $files */
/** @var Config $config */
$files = $this->wire('files');
$config = $this->wire('config');
$files = $this->wire()->files;
$config = $this->wire()->config;
$debug = $config->debug;
$configOptions = $config->imageSizerOptions;
$webpOptions = $config->webpOptions;
$createdVariationHookData = null; // populated as array only when new variation created (for createdVariation hook)
if(!empty($webpOptions['quality'])) $defaultOptions['webpQuality'] = $webpOptions['quality'];
if(!is_array($configOptions)) $configOptions = array();
@@ -765,7 +783,10 @@ class Pageimage extends Pagefile {
}
}
if($options['cropping'] === true && empty($options['cropExtra']) && $options['focus'] && $this->hasFocus && $width && $height) {
if($options['cropping'] === true
&& empty($options['cropExtra'])
&& $options['focus'] && $this->hasFocus
&& $width && $height) {
// crop to focus area
$focus = $this->focus();
if(is_int($options['zoom'])) $focus['zoom'] = $options['zoom']; // override
@@ -803,9 +824,12 @@ class Pageimage extends Pagefile {
$suffix = $options['suffix'];
sort($suffix);
foreach($suffix as $key => $s) {
$s = strtolower($this->wire('sanitizer')->fieldName($s));
if(empty($s)) unset($suffix[$key]);
else $suffix[$key] = $s;
$s = strtolower($this->wire()->sanitizer->fieldName($s));
if(empty($s)) {
unset($suffix[$key]);
} else {
$suffix[$key] = $s;
}
}
if(count($suffix)) $suffixStr = '-' . implode('-', $suffix);
}
@@ -867,7 +891,7 @@ class Pageimage extends Pagefile {
if(file_exists($filenameUnvalidated)) $files->unlink($filenameUnvalidated, true);
if(file_exists($filenameUnvalidatedWebp)) $files->unlink($filenameUnvalidatedWebp, true);
if(@copy($this->filename(), $filenameUnvalidated)) {
if($files->copy($this->filename(), $filenameUnvalidated)) {
try {
$timer = $debug ? Debug::timer() : null;
@@ -920,7 +944,7 @@ class Pageimage extends Pagefile {
$this->error = "ImageSizer::resize($width, $height) failed for $filenameUnvalidated";
}
if($debug && empty($options['webpOnly'])) $this->wire('log')->save('image-sizer',
if($debug && empty($options['webpOnly'])) $this->wire()->log->save('image-sizer',
str_replace('ImageSizerEngine', '', $sizer->getEngine()) . ' ' .
($this->error ? "FAILED Resize: " : "Resized: ") . "$originalName => " . basename($filenameFinal) . " " .
"({$width}x{$height}) " . Debug::timer($timer) . " secs $originalSize => " . filesize($filenameFinal) . " bytes " .
@@ -961,8 +985,8 @@ class Pageimage extends Pagefile {
// we also tell PW about it for logging and/or admin purposes
$this->error($this->error);
$logError = str_replace($config->paths->root, $config->urls->root, $filenameFinal) . " - $this->error";
$this->wire('log')->save('image-sizer', $logError);
$logError = str_replace($config->paths('root'), $config->urls('root'), $filenameFinal) . " - $this->error";
$this->wire()->log->save('image-sizer', $logError);
}
$pageimage->setFilename($filenameFinal);
@@ -1027,7 +1051,7 @@ class Pageimage extends Pagefile {
*
*/
public function sizeName($name, array $options = array()) {
$sizes = $this->wire('config')->imageSizes;
$sizes = $this->wire()->config->imageSizes;
if(!isset($sizes[$name])) throw new WireException("Unknown image size '$name' (not in \$config->imageSizes)");
$size = $sizes[$name];
$options = array_merge($size, $options);
@@ -1167,12 +1191,12 @@ class Pageimage extends Pagefile {
// return hidpi width intended: scale omitted or provided in $width argument
$scale = $width;
if(!$scale || $scale < 0 || $scale > 1) $scale = 0.5;
if(is_string($options) && $options === "100%") return $options;
if($options === "100%") return $options;
$width = is_array($options) ? 0 : (int) $options;
if($width < 1) $width = $this->width();
if($width === "100%") return $width;
return ceil($width * $scale);
} else if($width && is_int($width) && $width > 0) {
} else if($width && is_int($width)) {
// resize intended
if(!is_array($options)) $options = array();
return $this->hidpiSize((int) $width, 0, $options);
@@ -1290,7 +1314,7 @@ class Pageimage extends Pagefile {
$options['nameHeight'] = $height;
}
if($this->wire('config')->installed > 1513336849) {
if($this->wire()->config->installed > 1513336849) {
// New installations from 2017-12-15 forward use an "ms" suffix for images from maxSize() method
$suffix = isset($options['suffix']) ? $options['suffix'] : array();
if(!is_array($suffix)) $suffix = array();
@@ -1304,16 +1328,17 @@ class Pageimage extends Pagefile {
/**
* Get ratio of width divided by height
*
* @param int $precision Optionally specify a value >2 for custom precision (default=2) 3.0.211+
* @return float
* @since 3.0.154
*
*/
public function ratio() {
public function ratio($precision = 2) {
$width = $this->width();
$height = $this->height();
if($width === $height) return 1.0;
$ratio = $width / $height;
$ratio = round($ratio, 2);
$ratio = round($ratio, max(2, (int) $precision));
if($ratio > 99.99) $ratio = 99.99; // max allowed width>height ratio
if($ratio < 0.01) $ratio = 0.01; // min allowed height>width ratio
return $ratio;
@@ -1444,7 +1469,7 @@ class Pageimage extends Pagefile {
* @param array $options See options for getVariations() method to limit what variations are removed, plus these:
* - `dryRun` (bool): Do not remove now and instead only return the filenames of variations that would be deleted (default=false).
* - `getFiles` (bool): Return deleted filenames? Also assumed if the test option is used (default=false).
* @return $this|array Returns $this by default, or array of deleted filenames if the `getFiles` option is specified
* @return PageImageVariations|array Returns $this by default, or array of deleted filenames if the `getFiles` option is specified
*
*/
public function removeVariations(array $options = array()) {
@@ -1513,10 +1538,12 @@ class Pageimage extends Pagefile {
*
*/
public function copyToPath($path) {
$files = $this->wire()->files;
if(parent::copyToPath($path)) {
foreach($this->getVariations() as $variation) {
/** @var Pageimage $variation */
if(!is_file($variation->filename)) continue;
$this->wire('files')->copy($variation->filename, $path);
$files->copy($variation->filename, $path);
}
return true;
}
@@ -1609,8 +1636,7 @@ class Pageimage extends Pagefile {
}
}
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
$sanitizer = $this->wire()->sanitizer;
$image = $this;
$original = null;
$replacements = array();
@@ -1643,7 +1669,7 @@ class Pageimage extends Pagefile {
}
if(strpos($markup, '{class}')) {
$class = isset($options['class']) ? $this->wire('sanitizer')->entities($options['class']) : 'pw-pageimage';
$class = isset($options['class']) ? $sanitizer->entities($options['class']) : 'pw-pageimage';
$replacements["{class}"] = $class;
}
@@ -1683,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;
}
@@ -1728,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);
@@ -1745,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();
@@ -1815,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
*
@@ -1854,4 +1913,3 @@ class Pageimage extends Pagefile {
}
}

View File

@@ -247,13 +247,13 @@ class PageimageVariations extends Wire implements \IteratorAggregate, \Countable
if(is_readable($this->pagefiles->path . $f)) {
$info["{$name}Url"] = $this->pagefiles->url . $f;
$info["{$name}Path"] = $this->pagefiles->path . $f;
continue;
// continue;
}
}
if(empty($info['crop'])) {
// attempt to extract crop info from suffix
foreach($info['suffix'] as $key => $suffix) {
foreach($info['suffix'] as /* $key => */ $suffix) {
if(strpos($suffix, 'cropx') === 0) {
$info['crop'] = ltrim($suffix, 'crop'); // i.e. x123y456
}
@@ -337,6 +337,7 @@ class PageimageVariations extends Wire implements \IteratorAggregate, \Countable
$count = 0;
if(!$options['info'] && !$options['count']) {
/** @var Pageimages $variations */
$variations = $this->wire(new Pageimages($this->pagefiles->page));
}
@@ -493,7 +494,7 @@ class PageimageVariations extends Wire implements \IteratorAggregate, \Countable
} else if(strpos($s, 'cropx') === 0) {
// skip cropx suffix (already known from $info[crop])
unset($info['suffix'][$k]);
continue;
// continue;
} else if(strpos($s, 'pid') === 0 && preg_match('/^pid\d+$/', $s)) {
// allow pid123 to pass through
} else if(in_array($s, $suffix)) {
@@ -602,6 +603,8 @@ class PageimageVariations extends Wire implements \IteratorAggregate, \Countable
*
*/
public function remove(array $options = array()) {
$files = $this->wire()->files;
$defaults = array(
'dryRun' => false,
@@ -612,8 +615,6 @@ class PageimageVariations extends Wire implements \IteratorAggregate, \Countable
if(!empty($options['dryrun'])) $defaults['dryRun'] = $options['dryrun']; // case insurance
$options = array_merge($defaults, $options); // placement after getVariations() intended
/** @var WireFileTools $files */
$files = $this->wire('files');
$deletedFiles = array();
$this->removeExtras($this->pageimage, $deletedFiles, $options);
@@ -655,4 +656,4 @@ class PageimageVariations extends Wire implements \IteratorAggregate, \Countable
}
}
}
}
}

View File

@@ -63,6 +63,8 @@ class Pageimages extends Pagefiles {
* Per the WireArray interface, return a blank Pageimage
*
* #pw-internal
*
* @return Pageimage
*
*/
public function makeBlankItem() {
@@ -87,6 +89,7 @@ class Pageimages extends Pagefiles {
$base = ($pos ? substr($name, 0, $pos) : null);
foreach($this as $pagefile) {
/** @var Pageimage $pagefile */
if($base !== null && strpos($pagefile->basename, $base) !== 0) continue;
// they start the same, is it a variation?
if(!$pagefile->isVariation($name)) continue;
@@ -129,6 +132,7 @@ class Pageimages extends Pagefiles {
$basenames = array();
foreach($this as $pageimage) {
/** @var Pageimage $pageimage */
$name = $pageimage->basename;
$ext = $pageimage->ext;
$extensions[$name] = $ext;
@@ -234,6 +238,7 @@ class Pageimages extends Pagefiles {
unset($options['limit']);
}
foreach($this as $image) {
/** @var Pageimage $image */
$out .= $image->render($markup, $options);
if($limit > 0 && ++$n >= $limit) break;
}

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
@@ -52,6 +53,7 @@
* @method string setupPageName(Page $page, array $options = array()) Determine and populate a name for the given page. #pw-internal
* @method void insertBefore(Page $page, Page $beforePage) Insert one page as a sibling before another. #pw-advanced
* @method void insertAfter(Page $page, Page $afterPage) Insert one page as a sibling after another. #pw-advanced
* @method bool touch($pages, $options = null, $type = 'modified') Update page modification time to now (or the given modification time). #pw-group-manipulation
*
* METHODS PURELY FOR HOOKS
* ========================
@@ -61,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.
@@ -72,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.
@@ -86,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.
*
*/
@@ -195,6 +197,7 @@ class Pages extends Wire {
*
*/
public function __construct(ProcessWire $wire) {
parent::__construct();
$this->setWire($wire);
$this->debug = $wire->config->debug === Config::debugVerbose ? true : false;
$this->sortfields = $this->wire(new PagesSortfields());
@@ -687,7 +690,7 @@ class Pages extends Wire {
* @param string|array|Selectors $selector Specify selector to find first matching page ID
* @param bool|array $options Specify boolean true to return all pages columns rather than just IDs.
* Or specify array of options (see find method for details), `verbose` option can optionally be in array.
* @return int|string|array
* @return int|array
* @see Pages::get(), Pages::has(), Pages::findIDs()
* @since 3.0.156
*
@@ -754,22 +757,22 @@ 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) {
$parent_id = $parent->id;
} else if(is_int($parent) || ctype_digit("$parent")) {
$parent_id = (int) "$parent";
} else if(is_string($parent) && $parent) {
} else if(is_string($parent)) {
$parent_id = $this->has($parent);
}
if(!$parent_id) $parent_id = null;
}
if(count($options)) {
$options['template'] = $template && $template instanceof Template ? $template : null;
$options['template'] = $template instanceof Template ? $template : null;
$options['parent_id'] = $parent_id;
return $this->loader->getById($ids, $options);
} else {
@@ -824,7 +827,7 @@ class Pages extends Wire {
* - `uncacheAll` (boolean): Whether the memory cache should be cleared (default=true).
* - `resetTrackChanges` (boolean): Whether the page's change tracking should be reset (default=true).
* - `quiet` (boolean): When true, modified date and modified_users_id won't be updated (default=false).
* - `adjustName` (boolean): Adjust page name to ensure it is unique within its parent (default=false).
* - `adjustName` (boolean): Adjust page name to ensure it is unique within its parent (default=true).
* - `forceID` (integer): Use this ID instead of an auto-assigned one (new page) or current ID (existing page).
* - `ignoreFamily` (boolean): Bypass check of allowed family/parent settings when saving (default=false).
* - `noHooks` (boolean): Prevent before/after save hooks (default=false), please also use $pages->___save() for call.
@@ -865,6 +868,36 @@ 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' )
* ~~~~~
*
* @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
*
@@ -1011,7 +1044,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);
}
@@ -1398,7 +1431,7 @@ class Pages extends Wire {
*
*/
public function ___setupNew(Page $page) {
return $this->editor()->setupNew($page);
$this->editor()->setupNew($page);
}
/**
@@ -1653,13 +1686,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);
}
@@ -1683,12 +1716,12 @@ class Pages extends Wire {
/**
* Return a fuel or other property set to the Pages instance
*
* @param string $key
* @param string $name
* @return mixed
*
*/
public function __get($key) {
switch($key) {
public function __get($name) {
switch($name) {
// A-Z
case 'autojoin': return $this->loader->getAutojoin();
case 'cacher': return $this->cacher();
@@ -1708,7 +1741,7 @@ class Pages extends Wire {
case 'trasher': return $this->trasher();
case 'types': return $this->types();
}
return parent::__get($key);
return parent::__get($name);
}
/**
@@ -2196,6 +2229,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
*
@@ -2243,7 +2290,6 @@ class Pages extends Wire {
*
*/
public function ___trashReady(Page $page) {
if($page) {} // ignore
}
/**
@@ -2257,6 +2303,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)
@@ -2344,7 +2402,6 @@ class Pages extends Wire {
*
*/
public function ___deleteBranchReady(Page $page, array $options) {
if($page && $options) {}
}
/**
@@ -2361,7 +2418,6 @@ class Pages extends Wire {
*
*/
public function ___deletedBranch(Page $page, array $options, $numDeleted) {
if($page && $options) {}
$this->log("Deleted branch with $numDeleted page(s)", $page);
}
@@ -2388,6 +2444,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)
@@ -2425,7 +2504,6 @@ class Pages extends Wire {
*
*/
public function ___sorted(Page $page, $children = false, $total = 0) {
if($page && $children && $total) {}
}
/**
@@ -2596,5 +2674,3 @@ class Pages extends Wire {
public function ___savedPageOrField(Page $page, array $changes = array()) { }
}

View File

@@ -17,7 +17,7 @@
* Pages using templates that already define their access (determined by $template->useRoles)
* are omitted from the pages_access table, as they aren't necessary.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*
@@ -52,6 +52,7 @@ class PagesAccess extends Wire {
*
*/
public function __construct($item = null) {
parent::__construct();
if(!$item) return;
if($item instanceof Page) {
$this->updatePage($item);
@@ -75,9 +76,9 @@ class PagesAccess extends Wire {
$accessTemplates = $this->getAccessTemplates();
$parent_id = (int) $parent_id;
$accessTemplateID = (int) $accessTemplateID;
$database = $this->wire('database');
$database = $this->wire()->database;
if(!$accessTemplateID && $this->config->debug) $this->message("Rebuilding pages_access");
if(!$accessTemplateID && $this->wire()->config->debug) $this->message("Rebuilding pages_access");
if($parent_id == 1) {
// if we're going to be rebuilding the entire tree, then just delete all of them now
@@ -188,13 +189,11 @@ class PagesAccess extends Wire {
// this is the template where access is defined for this page
$accessParent = $page->getAccessParent();
$accessTemplate = $accessParent->template;
$database = $this->wire('database');
$database = $this->wire()->database;
if(!$accessParent->id || $accessParent->id == $page->id) {
// page is the same as the one that defines access, so it doesn't need to be here
$query = $database->prepare("DELETE FROM pages_access WHERE pages_id=:page_id");
$query->bindValue(":page_id", $page_id, \PDO::PARAM_INT);
$query->execute();
} else {
$template_id = (int) $accessParent->template->id;
@@ -204,11 +203,12 @@ class PagesAccess extends Wire {
"ON DUPLICATE KEY UPDATE templates_id=VALUES(templates_id) ";
$query = $database->prepare($sql);
$query->bindValue(":page_id", $page_id, \PDO::PARAM_INT);
$query->bindValue(":template_id", $template_id, \PDO::PARAM_INT);
$query->execute();
}
$query->bindValue(":page_id", $page_id, \PDO::PARAM_INT);
$query->execute();
if($page->numChildren > 0) {
if($page->parentPrevious && $accessParent->id != $page->id) {
@@ -231,7 +231,7 @@ class PagesAccess extends Wire {
*
*/
public function deletePage(Page $page) {
$database = $this->wire('database');
$database = $this->wire()->database;
$query = $database->prepare("DELETE FROM pages_access WHERE pages_id=:page_id");
$query->bindValue(":page_id", $page->id, \PDO::PARAM_INT);
$query->execute();

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
*
@@ -35,10 +35,11 @@ class PagesEditor extends Wire {
*
*/
public function __construct(Pages $pages) {
parent::__construct();
$this->pages = $pages;
$config = $pages->wire()->config;
if($config->dbStripMB4 && strtolower($config->dbEngine) != 'utf8mb4') {
if($config->dbStripMB4 && strtolower($config->dbCharset) != 'utf8mb4') {
$this->addHookAfter('Fieldtype::sleepValue', $this, 'hookFieldtypeSleepValueStripMB4');
}
}
@@ -200,7 +201,7 @@ class PagesEditor extends Wire {
if($saveable) foreach($page->getArray() as $key => $value) {
if($fieldName && $key != $fieldName) continue;
if(!$page->template->fieldgroup->getField($key)) continue;
if(is_object($value) && $value instanceof Wire && $value->isChanged()) {
if($value instanceof Wire && $value->isChanged()) {
$reason = $outputFormattingReason . " [$key]";
$saveable = false;
break;
@@ -352,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
@@ -362,14 +363,15 @@ class PagesEditor extends Wire {
// assign sort order
if($page->sort < 0) {
$page->sort = $page->parent->numChildren();
$page->sort = ($parent->id ? $parent->numChildren() : 0);
}
// assign any default values for fields
foreach($page->template->fieldgroup as $field) {
/** @var Field $field */
if($page->isLoaded($field->name)) continue; // value already set
if(!$page->hasField($field)) continue; // field not valid for page
if(!strlen("$field->defaultValue")) continue; // no defaultValue property defined with Fieldtype config inputfields
if(!strlen((string) $field->get('defaultValue'))) continue; // no defaultValue property defined with Fieldtype config inputfields
try {
$blankValue = $field->type->getBlankValue($page, $field);
if(is_object($blankValue) || is_array($blankValue)) continue; // we don't currently handle complex types
@@ -417,11 +419,14 @@ class PagesEditor extends Wire {
* - `uncacheAll` (boolean): Whether the memory cache should be cleared (default=true)
* - `resetTrackChanges` (boolean): Whether the page's change tracking should be reset (default=true)
* - `quiet` (boolean): When true, created/modified time+user will use values from $page rather than current user+time (default=false)
* - `adjustName` (boolean): Adjust page name to ensure it is unique within its parent (default=false)
* - `adjustName` (boolean): Adjust page name to ensure it is unique within its parent (default=true)
* - `forceID` (integer): Use this ID instead of an auto-assigned on (new page) or current ID (existing page)
* - `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
*
@@ -431,11 +436,13 @@ class PagesEditor extends Wire {
$defaultOptions = array(
'uncacheAll' => true,
'resetTrackChanges' => true,
'adjustName' => false,
'adjustName' => true,
'forceID' => 0,
'ignoreFamily' => false,
'noHooks' => false,
'noFields' => false,
'caller' => '',
'callback' => '',
);
if(is_string($options)) $options = Selectors::keyValueStringToArray($options);
@@ -443,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 = '';
@@ -455,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';
}
}
}
$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($options['adjustName']) $this->pages->names()->checkNameConflicts($page);
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;
}
@@ -559,7 +586,7 @@ class PagesEditor extends Wire {
}
}
if(isset($data['modified_users_id'])) $page->modified_users_id = $data['modified_users_id'];
$page->modified_users_id = $data['modified_users_id'];
if(isset($data['created_users_id'])) $page->created_users_id = $data['created_users_id'];
if(!$page->isUnpublished() && ($isNew || ($page->statusPrevious && ($page->statusPrevious & Page::statusUnpublished)))) {
@@ -715,6 +742,7 @@ class PagesEditor extends Wire {
// save each individual Fieldtype data in the fields_* tables
foreach($page->fieldgroup as $field) {
/** @var Field $field */
$fieldtype = $field->type;
$name = $field->name;
if($options['noFields'] || isset($corruptedFields[$name]) || !$fieldtype || !$page->hasField($field)) {
@@ -883,6 +911,8 @@ class PagesEditor extends Wire {
}
if($field->type->savePageField($page, $field)) {
// if page has a files path (or might have previously), trigger filesManager's save
if(PagefilesManager::hasPath($page)) $page->filesManager->save();
$page->untrackChange($field->name);
if(empty($options['quiet'])) {
$user = $this->wire()->user;
@@ -907,6 +937,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
*
@@ -954,7 +1068,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;
}
/**
@@ -1068,7 +1182,7 @@ class PagesEditor extends Wire {
}
/**
* Permanently delete a page and it's fields.
* Permanently delete a page and its fields.
*
* Unlike trash(), pages deleted here are not restorable.
*
@@ -1091,19 +1205,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.");
}
@@ -1114,17 +1244,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;
@@ -1135,10 +1269,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;
@@ -1148,17 +1292,17 @@ 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
* - set (array): Array of properties to set to the clone (you can also do this later)
* - recursionLevel (int): recursion level, for internal use only.
* @return Page the newly cloned page or a NullPage() with id=0 if unsuccessful.
* @return Page|NullPage the newly cloned page or a NullPage() with id=0 if unsuccessful.
* @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,
@@ -1184,6 +1328,7 @@ class PagesEditor extends Wire {
// Ensure all data is loaded for the page
foreach($page->fieldgroup as $field) {
/** @var Field $field */
if($page->hasField($field->name)) $page->get($field->name);
}
@@ -1274,11 +1419,13 @@ class PagesEditor extends Wire {
if($options['recursionLevel'] === 0) {
// update pages_parents table, only when at recursionLevel 0 since parents()->rebuild() already descends
/*
if($copy->numChildren) {
$copy->setIsNew(true);
$this->pages->parents()->rebuild($copy);
$copy->setIsNew(false);
}
*/
// update sort
if($copy->parent()->sortfield() == 'sort') {
$this->sortPage($copy, $copy->sort, true);
@@ -1380,7 +1527,7 @@ class PagesEditor extends Wire {
$sql .= ':time ';
}
if($user && $user instanceof User && ($col === 'modified' || $col === 'created')) {
if($user instanceof User && ($col === 'modified' || $col === 'created')) {
$sql .= ", {$col}_users_id=:user ";
}
@@ -1404,7 +1551,7 @@ class PagesEditor extends Wire {
* @param Page $child Page that you want to move.
* @param Page|int|string $parent Parent to move it under (may be Page object, path string, or ID integer).
* @param array $options Options to modify behavior (see PagesEditor::save for options).
* @return bool|array True on success or false if not necessary.
* @return bool True on success or false if not necessary.
* @throws WireException if given parent does not exist, or move is not allowed
*
*/
@@ -1673,10 +1820,12 @@ class PagesEditor extends Wire {
if($options['clearFields']) {
foreach($page->fieldgroup as $field) {
/** @var Field $field */
/** @var Fieldtype $fieldtype */
$fieldtype = $field->type;
if($options['clearMethod'] === 'delete') {
$result = $field->type->deletePageField($page, $field);
$result = $fieldtype->deletePageField($page, $field);
} else {
$result = $field->type->emptyPageField($page, $field);
$result = $fieldtype->emptyPageField($page, $field);
}
if(!$result) {
$errors[] = "Unable to clear field '$field' from page $page";
@@ -1690,7 +1839,13 @@ class PagesEditor extends Wire {
$error = "Error clearing files for page $page";
try {
if(PagefilesManager::hasPath($page)) {
if(!$page->filesManager->emptyAllPaths()) {
$filesManager = $page->filesManager();
if(!$filesManager) {
// $filesManager will be null if page has deleted status
// so create our own instance
$filesManager = new PagefilesManager($page);
}
if(!$filesManager->emptyAllPaths()) {
$errors[] = $error;
$halt = $options['haltOnError'];
}

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
@@ -970,14 +967,19 @@ class PagesExportImport extends Wire {
}
if(!$commitException) {
if($pageValue !== null && $fieldtypeImportOptions['returnsPageValue']) {
$page->set($field->name, $pageValue);
// @todo debug why FieldtypeTextLanguage requires a setAndSave() at this point.
//if($field->type == 'FieldtypePageTitleLanguage' || $field->type == 'FieldtypeTextLanguage') {
// $page->setAndSave($field->name, $pageValue);
// } else {
$page->set($field->name, $pageValue);
// }
} else if(!$fieldtypeImportOptions['returnsPageValue']) {
$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
@@ -1015,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;
}
@@ -1028,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;
@@ -1132,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");
@@ -1238,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
@@ -1295,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();
@@ -1311,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

@@ -87,6 +87,7 @@ class PagesLoader extends Wire {
*
*/
public function __construct(Pages $pages) {
parent::__construct();
$this->pages = $pages;
$this->debug = $pages->debug();
}
@@ -195,6 +196,8 @@ class PagesLoader extends Wire {
$selector = "id=$selector";
}
}
/** @var array|int|string $selector */
return $selector;
}
@@ -349,7 +352,7 @@ class PagesLoader extends Wire {
$selectors = $selector;
} else {
$selector = $this->normalizeSelector($selector, false);
$selectors = $this->wire(new Selectors());
$selectors = $this->wire(new Selectors()); /** @var Selectors $selectors */
$selectors->init($selector);
}
@@ -391,7 +394,7 @@ class PagesLoader extends Wire {
$pagesIDs = array();
if($debug) Debug::timer("$caller($selectorString)", true);
$profiler = $this->wire('profiler');
$profiler = $this->wire()->profiler;
$profilerEvent = $profiler ? $profiler->start("$caller($selectorString)", "Pages") : null;
if(($lazy || $findIDs) && strpos($selectorString, 'limit=') === false) $options['getTotal'] = false;
@@ -626,7 +629,8 @@ class PagesLoader extends Wire {
$languages = $this->wire()->languages;
$languageIds = array();
$templatesById = array();
$tmpAutojoinFields = array(); // fields to autojoin temporarily, just during this method call
if($languages) foreach($languages as $language) $languageIds[$language->id] = $language->id;
$options['findIDs'] = $useCache ? 4 : 3;
@@ -694,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);
}
@@ -702,16 +707,25 @@ class PagesLoader extends Wire {
$page->setForced($key, $value);
}
}
// set blank values where joinField didn't appear on page row
foreach($joinFields as $joinField) {
if(isset($row["{$joinField}__data"])) continue;
if(empty($joinResults[$joinField])) continue; // field did not support autojoin
if(!$template->fieldgroup->hasField($joinField)) continue;
$field = $page->getField($joinField);
if(!$field || !$field->type) continue;
$blankValue = $field->type->getBlankValue($page, $field);
$page->setFieldValue($field->name, $blankValue, false);
$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;
}
} else {
// set blank values where joinField didn't appear on page row
$blankValue = $field->type->getBlankValue($page, $field);
$page->setFieldValue($field->name, $blankValue, false);
}
}
$page->setIsLoaded(true);
@@ -728,6 +742,10 @@ class PagesLoader extends Wire {
$pageArray->setTotal($paginationTotal);
$pageArray->resetTrackChanges(true);
foreach($tmpAutojoinFields as $field) { /** @var Field $field */
$field->removeFlag(Field::flagAutojoin)->untrackChange('flags');
}
if($useCache) {
$selectorString = $pageArray->getSelectors(true);
$this->pages->cacher()->selectorCache($selectorString, $options, $pageArray);
@@ -771,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);
@@ -804,6 +828,109 @@ class PagesLoader extends Wire {
return $page && $page->id ? $page : $this->pages->newNullPage();
}
/**
* Find pages and cache the result for specified period of time
*
* Use this when you want to cache a slow or complex page finding operation so that it doesnt
* have to be repated for every web request. Note that this only caches the find operation
* and not the loading of the found pages.
*
* ~~~~~
* $items = $pages->findCache("title%=foo"); // 60 seconds (default)
* $items = $pages->findCache("title%=foo", 3600); // 1 hour
* $items = $pages->findCache("title%=foo", "+1 HOUR"); // same as above
* ~~~~~
*
* @param string|array|Selectors $selector
* @param int|string|bool|null $expire When the cache should expire, one of the following:
* - Max age integer (in seconds).
* - Any string accepted by PHPs `strtotime()` that specifies when the cache should be expired.
* - Any `WireCache::expire…` constant or anything accepted by the `WireCache::get()` $expire argument.
* @param array $options Options to pass to `$pages->getByIDs()`, or:
* - `findIDs` (bool): Return just the page IDs rather then the actual pages? (default=false)
* @return PageArray|array
* @since 3.0.218
*
*/
public function findCache($selector, $expire = 60, $options = array()) {
$user = $this->wire()->user;
$cache = $this->wire()->cache;
$ns = 'pages.findCache';
$items = null;
if(is_string($selector)) {
$selectorStr = $selector;
$selectors = $selector;
} else {
$selectors = $this->wire(new Selectors($selector));
$selectorStr = (string) $selectors;
}
$rolesStr = (string) $user->roles;
if(strpos($rolesStr, '|')) {
$rolesArray = explode('|', $rolesStr);
sort($rolesArray);
$rolesStr = implode('|', $rolesArray);
}
$optionsStr = '';
foreach($options as $key => $value) {
if(!is_string($value)) {
if(is_array($value)) $value = print_r($value, true);
$value = (string) $value;
}
$optionsStr .= "$key==$value,";
}
$cacheName = "$rolesStr\r$selectorStr\r$optionsStr";
$pageNum = $this->wire()->input->pageNum();
if($pageNum > 1 && Selectors::selectorHasField($selectors, 'limit')) {
if(!Selectors::selectorHasField($selectors, 'start')) $cacheName .= "\r$pageNum";
}
$cacheName = md5($cacheName);
$data = $cache->getFor($ns, $cacheName, $expire);
if(!empty($data) && $data['selector'] === $selectorStr && $data['roles'] === $rolesStr) {
$ids = $data['pages'];
} else {
$ids = null;
if(strpos($selectorStr, 'template') !== false && empty($options['template'])) {
$info = Selectors::selectorHasField($selectors, array('template', 'templates_id'), array('verbose' => true));
if($info['result']) $options['template'] = $this->wire()->templates->get($info['value']);
echo "template=$options[template]\n";
}
}
if($ids === null) {
if(empty($options['findIDs'])) {
$items = $this->find($selectors, $options);
$ids = $items->explode('id');
} else {
$ids = $this->pages->findIDs($selectors, $options);
}
$data = array(
'selector' => $selectorStr,
'roles' => $rolesStr,
'pages' => $ids
);
$cache->saveFor($ns, $cacheName, $data, $expire);
} else if(empty($options['findIDs'])) {
$items = $this->pages->getByIDs($ids, $options);
}
if(!empty($options['findIDs'])) return $ids;
foreach($items as $item) {
if($item instanceof NullPage || $item->status & Page::statusTrash) {
$items->remove($item);
}
}
return $items;
}
/**
* Returns the first page matching the given selector with no exclusions
*
@@ -934,7 +1061,7 @@ class PagesLoader extends Wire {
* just those used by the template. Optionally specify an $options array instead, see the method notes above.
* @param int|null $parent_id Specify a parent to make the load faster, as it reduces the possibility for full table scans.
* This argument is ignored when an options array is supplied for the $template.
* @return PageArray|Page Returns Page only if the 'getOne' option is specified, otherwise always returns a PageArray.
* @return PageArray|Page|NullPage Returns Page only if the 'getOne' option is specified, otherwise always returns a PageArray.
* @throws WireException
*
*/
@@ -1150,6 +1277,7 @@ class PagesLoader extends Wire {
if($options['autojoin'] && $this->autojoin) {
foreach($fields as $field) {
/** @var Field $field */
if(!empty($options['joinFields']) && in_array($field->name, $options['joinFields'])) {
// joinFields option specified to force autojoin this field
} else {
@@ -1160,7 +1288,8 @@ class PagesLoader extends Wire {
}
$table = $database->escapeTable($field->table);
// check autojoin not allowed, otherwise merge in the autojoin query
if(!$field->type || !$field->type->getLoadQueryAutojoin($field, $query)) continue;
$fieldtype = $field->type;
if(!$fieldtype || !$fieldtype->getLoadQueryAutojoin($field, $query)) continue;
// complete autojoin
$query->leftjoin("$table ON $table.pages_id=pages.id"); // QA
}
@@ -1194,7 +1323,7 @@ class PagesLoader extends Wire {
// page to populate, if provided in 'getOne' mode
/** @var Page|null $_page */
$_page = $options['getOne'] && $options['page'] && $options['page'] instanceof Page ? $options['page'] : null;
$_page = $options['getOne'] && $options['page'] instanceof Page ? $options['page'] : null;
try {
// while($page = $stmt->fetchObject($_class, array($template))) {
@@ -1233,7 +1362,7 @@ class PagesLoader extends Wire {
}
} catch(\Exception $e) {
$error = $e->getMessage() . " [pageClass=$class, template=$template]";
$user = $this->wire('user');
$user = $this->wire()->user;
if($user && $user->isSuperuser()) $this->error($error);
$this->wire()->log->error($error);
$this->trackException($e, false);
@@ -1415,6 +1544,11 @@ class PagesLoader extends Wire {
*
*/
public function getPath($id, $options = array()) {
$modules = $this->wire()->modules;
$database = $this->wire()->database;
$languages = $this->wire()->languages;
$config = $this->wire()->config;
$defaults = array(
'language' => null,
@@ -1430,7 +1564,7 @@ class PagesLoader extends Wire {
$options = array_merge($defaults, $options);
if(is_object($id) && $id instanceof Page) {
if($id instanceof Page) {
if($options['useCache']) return $id->path();
$id = $id->id;
}
@@ -1438,12 +1572,11 @@ class PagesLoader extends Wire {
$id = (int) $id;
if(!$id || $id < 0) return '';
$languages = $this->wire()->languages;
if($languages && !$languages->hasPageNames()) $languages = null;
$language = $options['language'];
$languageID = 0;
$homepageID = (int) $this->wire()->config->rootPageID;
$homepageID = (int) $config->rootPageID;
if(!empty($language) && $languages) {
if(is_string($language) || is_int($language)) $language = $languages->get($language);
@@ -1469,19 +1602,16 @@ class PagesLoader extends Wire {
}
// if PagePaths module is installed, and not in multi-language environment, attempt to get from PagePaths module
if(!$languages && !$languageID && $options['usePagePaths'] && $this->wire('modules')->isInstalled('PagePaths')) {
if(!$languages && !$languageID && $options['usePagePaths'] && $modules->isInstalled('PagePaths')) {
/** @var PagePaths $pagePaths */
$pagePaths = $this->modules->get('PagePaths');
$pagePaths = $modules->get('PagePaths');
$path = $pagePaths->getPath($id);
if($path) return $path;
} else {
$pagePaths = null;
}
$path = '';
$templatesID = 0;
$parentID = $id;
$database = $this->wire('database');
$maxParentID = $language ? 0 : 1;
$cols = 'parent_id, templates_id, name';
if($languageID) $cols .= ", name$languageID"; // col=3
@@ -1868,7 +1998,7 @@ class PagesLoader extends Wire {
/**
* Count and return how many pages will match the given selector string
*
* @param string|array $selector Specify selector, or omit to retrieve a site-wide count.
* @param string|array|Selectors $selector Specify selector, or omit to retrieve a site-wide count.
* @param array|string $options See $options in Pages::find
* @return int
*
@@ -1896,10 +2026,338 @@ class PagesLoader extends Wire {
$selector .= ", limit=1";
} else if(is_array($selector)) {
$selector['limit'] = 1;
} else if($selector instanceof Selectors) {
$selector->add(new SelectorEqual('limit', 1));
}
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
*
@@ -2032,4 +2490,4 @@ class PagesLoader extends Wire {
return $this->loading;
}
}
}

View File

@@ -45,6 +45,7 @@ class PagesLoaderCache extends Wire {
*
*/
public function __construct(Pages $pages) {
parent::__construct();
$this->pages = $pages;
}
@@ -83,12 +84,25 @@ class PagesLoaderCache extends Wire {
if(!ctype_digit("$id")) $id = str_replace('id=', '', $id);
if(ctype_digit("$id")) $id = (int) $id;
if(!isset($this->pageIdCache[$id])) return null;
/** @var Page $page */
$page = $this->pageIdCache[$id];
$page->setOutputFormatting($this->pages->outputFormatting);
$page = $this->pageIdCache[$id]; /** @var Page $page */
$of = $this->pages->loader()->getOutputFormatting();
if(!$of && $page === $this->wire()->page) return $page; // skip of() adjustment
$page->of($of);
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.
*
@@ -147,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;
@@ -308,7 +322,7 @@ class PagesLoaderCache extends Wire {
// cache non-default languages separately
if($this->wire()->languages) {
$language = $this->wire()->user->language;
if(!$language->isDefault()) {
if($language && !$language->isDefault && $language->name != 'default') {
$selector .= ", _lang=$language->id"; // for caching purposes only, not recognized by PageFinder
}
}
@@ -318,4 +332,4 @@ class PagesLoaderCache extends Wire {
return null;
}
}
}

View File

@@ -3,7 +3,7 @@
/**
* ProcessWire Pages Names
*
* ProcessWire 3.x, Copyright 2019 by Ryan Cramer
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
* #pw-summary This class includes methods for generating and modifying page names.
@@ -71,7 +71,7 @@ class PagesNames extends Wire {
public function __construct(Pages $pages) {
$this->pages = $pages;
$pages->wire($this);
$untitled = $this->wire('config')->pageNameUntitled;
$untitled = $this->wire()->config->pageNameUntitled;
if($untitled) $this->untitledPageName = $untitled;
$this->nameMaxLength = Pages::nameMaxLength;
parent::__construct();
@@ -228,7 +228,7 @@ class PagesNames extends Wire {
// default format is title (when the page has one)
$format = 'title';
} else if($this->wire('languages') && $page->title instanceof LanguagesValueInterface) {
} else if($this->wire()->languages && $page->title instanceof LanguagesValueInterface) {
// check for multi-language title
/** @var LanguagesPageFieldValue $pageTitle */
$pageTitle = $page->title;
@@ -616,8 +616,8 @@ class PagesNames extends Wire {
$languages = $options['multilang'] || $options['language'] ? $this->wire()->languages : null;
if($languages && !$languages->hasPageNames()) $languages = null;
if($this->wire('config')->pageNameCharset == 'UTF8') {
$name = $this->wire('sanitizer')->pageName($name, Sanitizer::toAscii);
if($this->wire()->config->pageNameCharset == 'UTF8') {
$name = $this->wire()->sanitizer->pageName($name, Sanitizer::toAscii);
}
$wheres = array();
@@ -649,7 +649,7 @@ class PagesNames extends Wire {
}
$sql = 'SELECT COUNT(*) FROM pages WHERE ' . implode(' AND ', $wheres);
$query = $this->wire('database')->prepare($sql);
$query = $this->wire()->database->prepare($sql);
foreach($binds as $key => $value) {
$query->bindValue($key, $value);

View File

@@ -15,7 +15,7 @@
* $numRows = $pages->parents()->rebuildAll();
* ~~~~~~
*
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
* @since 3.0.156
@@ -45,6 +45,7 @@ class PagesParents extends Wire {
*
*/
public function __construct(Pages $pages) {
parent::__construct();
$this->pages = $pages;
$this->debug = $pages->debug();
}
@@ -174,7 +175,7 @@ class PagesParents extends Wire {
// more parents to go, get rest recursively
$o = $options;
$o['columns'] = $columns;
foreach($this->getParents($lastParentID, $o) as $key => $value) {
foreach($this->getParents($lastParentID, $o) as $value) {
$values[] = $value;
}
}
@@ -224,6 +225,8 @@ class PagesParents extends Wire {
*/
public function findParents(array $options = array()) {
$database = $this->wire()->database;
static $calls = 0;
$defaults = array(
@@ -245,7 +248,6 @@ class PagesParents extends Wire {
$options = array_merge($defaults, $options);
$parentID = isset($options['parent']) ? (int) "$options[parent]" : 0;
$database = $this->wire('database'); /** @var WireDatabasePDO $database */
$sql = array();
$bind = array();
$column = $options['column'];
@@ -447,7 +449,7 @@ class PagesParents extends Wire {
JOIN pages AS parents on pages.parent_id=parents.id AND parents.parent_id>=:id
GROUP BY pages.parent_id
";
$query = $this->database->prepare(trim($sql));
$query = $this->wire()->database->prepare(trim($sql));
$query->bindValue(':id', $minParentID, \PDO::PARAM_INT);
$query->execute();
while($row = $query->fetch(\PDO::FETCH_NUM)) {
@@ -488,18 +490,21 @@ class PagesParents extends Wire {
// homepage not maintained in pages_parents table
// pages being cloned are not maintained till clone operation finishes
if($page->id < 2 || $page->_cloning || !$page->parent) return 0;
if($page->id < 2 || !$page->parent) return 0;
// if($page->_cloning) return 0;
// first check if page parents need any updates
if($page->isNew()) {
// newly added page
if($page->parent->numChildren === 1) {
// first time parent gets added to pages_parents
$numRows += $this->rebuild($page->parent);
$numRows += $this->addParent($page->parent);
// $numRows += $this->rebuild($page->parent);
}
} else if($page->parentPrevious && $page->parentPrevious->id != $page->parent->id) {
// existing page with parent changed
$this->rebuildAll();
$numRows += $this->movePage($page, $page->parentPrevious, $page->parent);
// $this->rebuildAll();
/*
if($page->parentPrevious->numChildren === 0) {
// parent no longer has children and doesnt need entry
@@ -529,7 +534,7 @@ class PagesParents extends Wire {
public function rebuild(Page $page) {
$pages_id = (int) $page->id;
$database = $this->wire('database'); /** @var WireDatabasePDO $database */
$database = $this->wire()->database;
$inserts = array();
$rowCount = 0;
@@ -558,6 +563,165 @@ class PagesParents extends Wire {
return $rowCount;
}
/**
* Rebuild pages_parents table for given page (experimental faster alternative/rewrite of rebuild method)
*
* #pw-internal
*
* @param Page $page
* @param Page $oldParent
* @param Page $newParent
* @return int
* @throws WireException
* @since 3.0.212
*
*/
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');
array_shift($oldParentIds); // shift off id=1
$oldParentIds[] = $oldParent->id;
$newParentIds = $newParent->parents()->explode('id');
array_shift($newParentIds); // shift off id=1
$newParentIds[] = $newParent->id;
// update the one page that moved
$sql = 'UPDATE pages_parents SET parents_id=:new_parent_id WHERE pages_id=:pages_id AND parents_id=:old_parent_id';
$query = $database->prepare($sql);
$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);
try {
$query->execute();
} catch(\Exception $e) {
if($e->getCode() != 23000) throw $e;
}
$numRows += $query->rowCount();
// find children and descendents of the page that moved
$sql = 'SELECT pages_id FROM pages_parents WHERE parents_id=:pages_id';
$query = $database->prepare($sql);
$query->bindValue(':pages_id', $page->id, \PDO::PARAM_INT);
$query->execute();
$ids = array($page->id => $page->id);
while($row = $query->fetch(\PDO::FETCH_NUM)) {
$id = (int) $row[0];
$ids[$id] = $id;
}
$query->closeCursor();
$inserts = array();
foreach($ids as $id) {
foreach($newParentIds as $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 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);
$sql = "DELETE FROM pages_parents WHERE pages_id IN($idStr) AND parents_id IN($oldParentIdStr)";
$database->exec($sql);
}
if(!count($inserts)) return $numRows;
$sql = "INSERT INTO pages_parents SET pages_id=:pages_id, parents_id=:parents_id";
$query = $database->prepare($sql);
foreach($inserts as $insert) {
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) {
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
*
* #pw-internal
*
* @param Page $page
* @return int
* @since 3.0.212
*
*/
protected function addParent(Page $page) {
// if page has no children it does not need pages_parents entries
if(!$page->numChildren) return 0;
$database = $this->wire()->database;
$numRows = 0;
$pageId = (int) $page->id;
$inserts = array();
// identify parents to store for $page
foreach($page->parents() as $parent) {
$parentId = (int) $parent->id;
if($parentId < 2) continue;
$inserts[] = array('pages_id' => $pageId, 'parents_id' => $parentId);
}
if(!count($inserts)) return 0;
$sql = "INSERT INTO pages_parents SET pages_id=:pages_id, parents_id=:parents_id";
$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);
try {
if($query->execute()) $numRows++;
} catch(\Exception $e) {
// ok
}
}
return $numRows;
}
/**
* Rebuild pages_parents branch starting at $fromParent and into all descendents
*
@@ -579,13 +743,13 @@ class PagesParents extends Wire {
*/
public function rebuildAll($fromParent = null) {
$database = $this->wire('database'); /** @var WireDatabasePDO $database */
$database = $this->wire()->database;
$inserts = array();
$parents = $this->findParentIDs($fromParent ? $fromParent : -2); // find parents within children
$rowCount = 0;
foreach($parents as $pages_id => $parents_id) {
if(isset($this->excludeIDs[$parents_id])) continue;
// if(isset($this->excludeIDs[$parents_id])) continue;
$inserts[] = "$pages_id,$parents_id";
while(isset($parents[$parents_id])) {
$parents_id = $parents[$parents_id];
@@ -620,7 +784,7 @@ class PagesParents extends Wire {
*/
public function clear($page) {
$pages_id = (int) "$page";
$database = $this->wire('database'); /** @var WireDatabasePDO $database */
$database = $this->wire()->database;
$query = $database->prepare("DELETE FROM pages_parents WHERE pages_id=:id");
$query->bindValue(':id', $pages_id, \PDO::PARAM_INT);
$query->execute();
@@ -639,7 +803,7 @@ class PagesParents extends Wire {
*/
public function delete($page) {
$pages_id = (int) "$page";
$database = $this->wire('database'); /** @var WireDatabasePDO $database */
$database = $this->wire()->database;
$sql = "DELETE FROM pages_parents WHERE pages_id=:pages_id OR parents_id=:parents_id";
$query = $database->prepare($sql);
$query->bindValue(':pages_id', $pages_id, \PDO::PARAM_INT);
@@ -666,7 +830,7 @@ class PagesParents extends Wire {
$id = (int) "$page";
$path = $page->path;
$database = $this->wire('database'); /** @var WireDatabasePDO $database */
$database = $this->wire()->database;
$tests = array(
'query-for-parents-of-page' => array(
@@ -754,4 +918,4 @@ class PagesParents extends Wire {
return $children;
}
}
}

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();
@@ -522,8 +538,8 @@ class PagesPathFinder extends Wire {
}
if(stripos($lastPart, 'index.') === 0 && preg_match('/^index\.(php\d?|s?html?)$/i', $lastPart)) {
array_pop($parts); // removing last part will force a 301
$this->addResultError('indexFile', 'Path had index file');
// index file will be allowed as URL segment, or 301 redirect if not allowed as URL segment
$this->addResultError('indexFile', 'Path had index file', true);
}
if($result['response'] < 400 && count($badNames)) {
@@ -572,7 +588,7 @@ class PagesPathFinder extends Wire {
$this->addResultNote("Detected language '$language->name' from first segment '$segment'");
$this->setResultLanguage($language, $segment);
if($this->verbose && $languageKey !== false) {
if($this->verbose) {
$this->result['parts'][] = array(
'type' => 'language',
'value' => $segment,
@@ -582,7 +598,7 @@ class PagesPathFinder extends Wire {
// reduce to just applicable language to limit name columns
// searched for by getPagesRow() method
if($language) $this->useLanguages = array($language);
$this->useLanguages = array($language);
return $language;
}
@@ -639,7 +655,7 @@ class PagesPathFinder extends Wire {
* Update paths for template info like urlSegments and pageNum and populate urls property
*
* @param string $path
* @return bool|string
* @return bool
*
*/
protected function applyResultTemplate($path) {
@@ -720,7 +736,14 @@ class PagesPathFinder extends Wire {
$_path = $path;
if(strlen($appendPath)) $path = rtrim($path, '/') . $appendPath;
if($fail || $_path !== $path) $result['redirect'] = '/' . ltrim($path, '/');
if($fail || $_path !== $path || ($hadTrailingSlash && $useTrailingSlash < 0)) {
if($fail && isset($result['errors']['indexFile']) && count($result['urlSegments']) === 1) {
// allow for an /index.php or /index.html type urlSegmentStr to redirect rather than fail
$fail = false;
}
$result['redirect'] = '/' . ltrim($path, '/');
}
$result['pathAdd'] = $appendPath;
@@ -782,8 +805,8 @@ class PagesPathFinder extends Wire {
// if there were any non-default language segments, let that dictate the language
if(empty($result['language']['segment'])) {
$useLangName = 'default';
foreach($result['parts'] as $key => $part) {
$useLangName = count($result['parts']) ? 'default' : $result['language']['name'];
foreach($result['parts'] as $part) {
$langName = $part['language'];
if(empty($langName) || $langName === 'default') continue;
$useLangName = $langName;
@@ -927,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
@@ -1415,9 +1438,8 @@ class PagesPathFinder extends Wire {
$this->admin = true;
} else {
$template = $this->getResultTemplate();
if(!$template) {
return false; // may need to detect later
} if(in_array($template->name, $config->adminTemplates, true)) {
if(!$template) return false; // may need to detect later
if(in_array($template->name, $config->adminTemplates, true)) {
$this->admin = true;
} else if(in_array($template->name, array('user', 'role', 'permission', 'language'))) {
$this->admin = true;
@@ -1474,10 +1496,11 @@ class PagesPathFinder extends Wire {
*
* @param string $name
* @param string $message
* @param bool $force Force add even if not in verbose mode? (default=false)
*
*/
protected function addResultError($name, $message) {
if(!$this->verbose) return;
protected function addResultError($name, $message, $force = false) {
//if(!$this->verbose && !$force) return;
$this->result['errors'][$name] = $message;
}
@@ -1663,7 +1686,7 @@ class PagesPathFinder extends Wire {
* Return Languages if installed w/languageSupportPageNames module or blank array if not
*
* @param bool $getArray Force return value as an array indexed by language name
* @return Languages|array
* @return Languages|Language[]
*
*/
protected function languages($getArray = false) {
@@ -2098,7 +2121,7 @@ class PagesPathFinderTests extends Wire {
$defaultPath = $item->path();
if($languages) {
foreach($languages as $language) {
// $user->setLanguage($language);
/** @var Language $language */
$path = $item->localPath($language);
if($language->isDefault() || $path === $defaultPath) {
$expect = 200;
@@ -2131,4 +2154,4 @@ class PagesPathFinderTests extends Wire {
}
return $testResults;
}
}
}

View File

@@ -23,6 +23,7 @@ class PagesRaw extends Wire {
*
*/
public function __construct(Pages $pages) {
parent::__construct();
$this->pages = $pages;
}
@@ -301,6 +302,12 @@ class PagesRawFinder extends Wire {
*
*/
protected $childrenFields = array();
/**
* @var array
*
*/
protected $templateFields = array();
/**
* @var array
@@ -381,7 +388,7 @@ class PagesRawFinder extends Wire {
/**
* IDs of pages to find, becomes array once known
*
* @var null|array
* @var null|array|string
*
*/
protected $ids = null;
@@ -393,6 +400,7 @@ class PagesRawFinder extends Wire {
*
*/
public function __construct(Pages $pages) {
parent::__construct();
$this->pages = $pages;
}
@@ -560,6 +568,12 @@ class PagesRawFinder extends Wire {
$this->unsetFields['parent_id'] = 'parent_id';
}
if(count($this->templateFields) && !isset($this->nativeFields['templates_id'])) {
// we need templates_id if finding any template properties
$this->nativeFields['templates_id'] = 'templates_id';
$this->unsetFields['templates_id'] = 'templates_id';
}
// requested native pages table fields/properties
if(count($this->nativeFields) || $this->getAll || $this->getPaths) {
// one or more native pages table column(s) requested
@@ -580,6 +594,11 @@ class PagesRawFinder extends Wire {
if(count($this->parentFields)) {
$this->findParent();
}
// requested template fields
if(count($this->templateFields)) {
$this->findTemplate();
}
// remove runtime only fields
if(count($this->unsetFields)) {
@@ -614,7 +633,9 @@ class PagesRawFinder extends Wire {
if($this->options['flat']) {
$delimiter = is_string($this->options['flat']) ? $this->options['flat'] : '.';
foreach($this->values as $key => $value) {
$this->values[$key] = $this->flattenValues($value, '', $delimiter);
if(is_array($value)) {
$this->values[$key] = $this->flattenValues($value, '', $delimiter);
}
}
}
@@ -665,7 +686,7 @@ class PagesRawFinder extends Wire {
// Array where [ 'field' => [ 'subfield' ]]
$colName = $fieldName; // array
$fieldName = $key;
if($fieldName === 'parent' || $fieldName === 'children') {
if($fieldName === 'parent' || $fieldName === 'children' || $fieldName === 'template') {
// passthru
} else if(in_array($fieldName, $runtimeNames) && !$fields->get($fieldName)) {
// passthru
@@ -690,7 +711,7 @@ class PagesRawFinder extends Wire {
list($fieldName, $colName) = explode('[', $fieldName, 2);
$colName = rtrim($colName, ']');
}
if($fieldName === 'parent' || $fieldName === 'children') {
if($fieldName === 'parent' || $fieldName === 'children' || $fieldName === 'template') {
// passthru
} else if(in_array($fieldName, $runtimeNames) && !$fields->get($fieldName)) {
// passthru
@@ -714,6 +735,9 @@ class PagesRawFinder extends Wire {
// @todo not yet supported
$this->childrenFields[$fullName] = $colName;
} else if($fieldName === 'template') {
$this->templateFields[$fullName] = $colName;
} else if($fullName === 'url' || $fullName === 'path') {
if($this->wire()->modules->isInstalled('PagePaths')) {
$this->runtimeFields[$fullName] = $fullName;
@@ -757,6 +781,8 @@ class PagesRawFinder extends Wire {
$templatesById = array();
$getPaths = $this->getPaths;
if(empty($this->selector)) return;
foreach($this->findIDs($this->selector, '*') as $row) {
$id = (int) $row['id'];
$this->ids[$id] = $id;
@@ -811,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;
}
@@ -835,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();
@@ -907,7 +933,7 @@ class PagesRawFinder extends Wire {
if(wireInstanceOf($field->type, 'FieldtypeOptions')) $getExternal = true;
} else {
foreach($cols as $key => $col) {
foreach($cols as $col) {
$col = $sanitizer->name($col);
if(empty($col)) continue;
if(isset($schema[$col])) {
@@ -1104,7 +1130,7 @@ class PagesRawFinder extends Wire {
*/
protected function findCustomFieldtypePage(Field $field, $fieldName, array $pageRefCols) {
$pageRefIds = array();
foreach($this->values as $pageId => $row) {
foreach($this->values as /* $pageId => */ $row) {
if(!isset($row[$fieldName])) continue;
$pageRefIds = array_merge($pageRefIds, $row[$fieldName]);
}
@@ -1118,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) {
@@ -1193,6 +1220,48 @@ class PagesRawFinder extends Wire {
}
}
/**
* Find and apply values for template.[property]
*
* @since 3.0.206
*
*/
protected function findTemplate() {
$templates = $this->wire()->templates;
$templateFields = $this->templateFields;
$templateData = array();
$templateIds = array();
foreach($this->values as /* $pageId => */ $data) {
$templateId = $data['templates_id'];
if(!isset($templateIds[$templateId])) $templateIds[$templateId] = $templateId;
}
foreach($templateIds as $templateId) {
$template = $templates->get($templateId);
$templateData[$templateId] = array();
foreach($templateFields as /* $fullName => */ $colName) {
if(empty($colName)) $colName = 'name';
$value = $template->get($colName);
if(is_object($value)) continue; // object values not allowed here
$templateData[$templateId][$colName] = $value;
}
}
if(!$this->getMultiple && count($this->templateFields) < 2) {
$colName = reset($this->templateFields);
foreach($templateData as $templateId => $data) {
$templateData[$templateId] = $data[$colName];
}
}
foreach($this->values as $pageId => $data) {
$templateId = $data['templates_id'];
$this->values[$pageId]['template'] = $templateData[$templateId];
}
}
/**
* Find runtime generated fields
*
@@ -1333,10 +1402,11 @@ class PagesRawFinder extends Wire {
$references = $fromPageIds[$toPageId];
} else {
$references = array();
foreach($fromPageIds[$toPageId] as $fieldName => $ids) {
foreach($fromPageIds[$toPageId] as /* $fieldName => */ $ids) {
$references = array_merge($references, $ids);
}
}
if(!$this->options['indexed']) $references = array_values($references);
$this->values[$toPageId]['references'] = $references;
}
return;
@@ -1360,9 +1430,17 @@ class PagesRawFinder extends Wire {
if(!isset($this->values[$toPageId]['references'][$fieldName])) {
$this->values[$toPageId]['references'][$fieldName] = array();
}
$this->values[$toPageId]['references'][$fieldName][$fromId] = $row;
if($this->options['indexed']) {
$this->values[$toPageId]['references'][$fieldName][$fromId] = $row;
} else {
$this->values[$toPageId]['references'][$fieldName][] = $row;
}
} else {
$this->values[$toPageId]['references'][$fromId] = $row;
if($this->options['indexed']) {
$this->values[$toPageId]['references'][$fromId] = $row;
} else {
$this->values[$toPageId]['references'][] = $row;
}
}
}
}
@@ -1684,4 +1762,4 @@ class PagesRawFinder extends Wire {
$s = "Unknown$context name(s) for findRaw: " . implode(', ', $fieldNames);
throw new WireException($s);
}
}
}

View File

@@ -11,7 +11,7 @@
* ~~~~~
* #pw-body
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @method Page|NullPage getPage()
@@ -180,6 +180,7 @@ class PagesRequest extends Wire {
*
*/
public function __construct(Pages $pages) {
parent::__construct();
$this->pages = $pages;
$this->config = $pages->wire()->config;
$this->init();
@@ -274,7 +275,6 @@ class PagesRequest extends Wire {
$input = $this->wire()->input;
$languages = $this->wire()->languages;
$page = null;
// get the requested path
$path = $this->getRequestPath();
@@ -301,11 +301,16 @@ class PagesRequest extends Wire {
// populate request path to class as other methods will now use it
$this->setRequestPath($path);
// determine if index.php is referenced in URL
if(stripos($this->dirtyUrl, 'index.php') !== false && stripos($path, 'index.php') === false) {
// this will force pathFinder to detect a redirect condition
$path = rtrim($path, '/') . '/index.php';
// determine if original URL had anything filtered out of path that will suggest a redirect
list($dirtyUrl,) = explode('?', "$this->dirtyUrl?", 2); // exclude query string
if(stripos($dirtyUrl, 'index.php') !== false && stripos($path, 'index.php') === false) {
// force pathFinder to detect a redirect condition without index.php
$dirtyUrl = strtolower(rtrim($dirtyUrl, '/'));
if(substr("/$dirtyUrl", -10) === '/index.php') $path = rtrim($path, '/') . '/index.php';
} else if(strpos($dirtyUrl, '//') !== false) {
// force pathFinder to detect redirect sans double slashes, /page/path// => /page/path/
$path = rtrim($path, '/') . '//';
}
// get info about requested path
@@ -398,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
@@ -569,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";
@@ -705,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 2020 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
*
@@ -25,6 +33,7 @@ class PagesTrash extends Wire {
*
*/
public function __construct(Pages $pages) {
parent::__construct();
$this->pages = $pages;
}
@@ -40,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);
@@ -69,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;
@@ -86,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;
@@ -106,18 +120,28 @@ class PagesTrash extends Wire {
*
*/
public function restore(Page $page, $save = true) {
$info = $this->getRestoreInfo($page, true);
if(!$info['restorable']) return false;
if($page->parent->isTrash()) {
if($save) $page->save();
} else {
if($info['restorable']) {
// we detected original parent
if($this->lastAction !== "restore:$page") $this->pages->restoreReady($page);
} 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;
@@ -171,7 +195,7 @@ class PagesTrash extends Wire {
$name = $result['name'];
$trashPrefix = $result['prefix']; // pageID.parentID.sort_ prefix for testing other language names later
$newParent = null;
$newParent = null; // auto-detected new parent, or null if new parent already on $page
$parentID = $result['parent_id'];
$sort = $result['sort'];
@@ -184,7 +208,7 @@ class PagesTrash extends Wire {
$info['notes'][] = 'Original parent no longer exists';
}
} else {
$info['notes'][] = 'Page root parent is not trash';
$info['notes'][] = 'Page root parent is not trash or page already has new parent';
}
} else if($parentID) {
@@ -198,47 +222,50 @@ class PagesTrash extends Wire {
$info['parent'] = $newParent ? $newParent : $this->pages->newNullPage();
$info['parent_id'] = $parentID;
$info['sort'] = $sort;
// if we have no new parent available we can exit now
if(!$newParent) {
$info['notes'][] = 'Unable to determine parent to restore to';
return $info;
}
// check if there is already a page at the restore location with the same name
$namePrevious = $name;
$name = $this->pages->names()->uniquePageName($name, $page, array('parent' => $newParent));
if($name !== $namePrevious) {
$info['notes'][] = "Name changed from '$namePrevious' to '$name' to be unique in new parent";
$info['namePrevious'] = $namePrevious;
$namePrevious = $name;
$nameParent = $newParent ? $newParent : $page->parent;
if($newParent || $this->pages->count("parent=$nameParent, name=$name, id!=$page->id, include=all")) {
// check if there is already a page at the restore location with the same name
$name = $this->pages->names()->uniquePageName($name, $page, array('parent' => $nameParent));
if($name !== $namePrevious) {
$info['notes'][] = "Name changed from '$namePrevious' to '$name' to be unique in new parent";
$info['namePrevious'] = $namePrevious;
}
}
$info['name'] = $name;
$info['restorable'] = true;
$info['restorable'] = $newParent !== null;
if($populateToPage) {
$page->name = $name;
$page->parent = $newParent;
$page->sort = $sort;
$page->removeStatus(Page::statusTrash);
if($newParent) {
$page->sort = $sort;
$page->parent = $newParent;
}
}
// do the same for other languages, when applicable
foreach($languages as $language) {
/** @var Language $language */
if($language->isDefault()) continue;
$langName = (string) $page->get("name$language->id");
$langKey = "name$language->id";
$langName = (string) $page->get($langKey);
if(!strlen($langName)) continue;
if(strpos($langName, $trashPrefix) === 0) {
list(,$langName) = explode('_', $langName);
}
$langNamePrevious = $langName;
$langName = $this->pages->names()->uniquePageName($langName, $page, array(
'parent' => $newParent,
'language' => $language
));
if($populateToPage) $page->set("name$language->id", $langName);
$info["name$language->id"] = $langName;
if($this->pages->count("parent=$nameParent, $langKey=$langName, id!=$page->id, include=all")) {
$langName = $this->pages->names()->uniquePageName($langName, $page, array(
'parent' => $nameParent,
'language' => $language
));
if($populateToPage) $page->set($langKey, $langName);
}
$info[$langKey] = $langName;
if($langName !== $langNamePrevious) {
$info['notes'][] = $language->get('title|name') . ' ' .
"name changed from '$langNamePrevious' to '$langName' to be unique in new parent";
@@ -333,7 +360,7 @@ class PagesTrash extends Wire {
$startTime = time();
$stopTime = $options['timeLimit'] ? $startTime + $options['timeLimit'] : false;
$stopNow = false;
$database = $this->wire('database');
$database = $this->wire()->database;
$useTransaction = $database->supportsTransaction();
$options['stopTime'] = $stopTime; // for pass2
$timeExpired = false;
@@ -449,7 +476,7 @@ class PagesTrash extends Wire {
}
if($totalDeleted || $options['verbose']) {
$numTrashChildren = $this->wire('pages')->trasher()->getTrashTotal();
$numTrashChildren = $this->pages->trasher()->getTrashTotal();
// return a negative number if pages still remain in trash
if($numTrashChildren && !$options['verbose']) $totalDeleted = $totalDeleted * -1;
} else {
@@ -540,7 +567,7 @@ class PagesTrash extends Wire {
*
*/
public function getTrashPage() {
$trashPageID = $this->wire('config')->trashPageID;
$trashPageID = $this->wire()->config->trashPageID;
$trashPage = $this->pages->get((int) $trashPageID);
if(!$trashPage->id || $trashPage->id != $trashPageID) {
throw new WireException("Cannot find trash page $trashPageID");
@@ -548,4 +575,4 @@ class PagesTrash extends Wire {
return $trashPage;
}
}
}

View File

@@ -13,7 +13,7 @@
* #pw-body
* #pw-use-constructor
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @method Page add($name)
@@ -101,7 +101,7 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
if(WireArray::iterable($templates)) {
// array already provided
foreach($templates as $template) {
if(is_int($template) || !$template instanceof Template) $template = $this->wire('templates')->get($template);
if(!$template instanceof Template) $template = $this->wire()->templates->get($template);
if(!$template) continue;
$this->templates[$template->id] = $template;
}
@@ -111,7 +111,7 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
$this->templates[$templates->id] = $templates;
} else {
// template id or template name
$template = $this->wire('templates')->get($templates);
$template = $this->wire()->templates->get($templates);
if($template) $this->templates[$template->id] = $template;
}
}
@@ -134,9 +134,9 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
} else if(is_string($parent) && ctype_digit($parent)) {
$id = (int) $parent;
} else if(is_string($parent)) {
$parent = $this->wire('pages')->get($parent, array('loadOptions' => array('autojoin' => false)));
$parent = $this->wire()->pages->get($parent, array('loadOptions' => array('autojoin' => false)));
$id = $parent->id;
} else if(is_object($parent) && $parent instanceof Page) {
} else if($parent instanceof Page) {
$id = $parent->id;
} else {
$id = 0;
@@ -333,8 +333,7 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
if(!isset($options['loadOptions'])) $options['loadOptions'] = array();
$options['loadOptions'] = $this->getLoadOptions($options['loadOptions']);
if(empty($options['caller'])) $options['caller'] = $this->className() . ".find($selectorString)";
$pages = $this->wire('pages')->find($this->selectorString($selectorString), $options);
/** @var PageArray $pages */
$pages = $this->wire()->pages->find($this->selectorString($selectorString), $options);
foreach($pages as $page) {
if(!$this->isValid($page)) {
$pages->remove($page);
@@ -358,7 +357,7 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
public function findIDs($selectorString, $options = array()) {
if(!isset($options['findAll'])) $options['findAll'] = true;
if(empty($options['caller'])) $options['caller'] = $this->className() . ".findIDs($selectorString)";
$ids = $this->wire('pages')->findIDs($this->selectorString($selectorString), $options);
$ids = $this->wire()->pages->findIDs($this->selectorString($selectorString), $options);
return $ids;
}
@@ -435,6 +434,11 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
* - This is the same as calling $page->save()
* - If the page is new, it will be inserted. If existing, it will be updated.
* - If you want to just save a particular field in a Page, use `$page->save($fieldName)` instead.
*
* Hook note:
* If you want to hook this method, please hook the `saveReady`, `saved`, or one of
* the `Pages::save*` methods instead, as hooking this method will not hook relevant pages
* saved directly through $pages->save().
*
* @param Page $page
* @return bool True on success
@@ -443,7 +447,7 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
*/
public function ___save(Page $page) {
if(!$this->isValid($page)) throw new WireException($this->errors('first'));
return $this->wire('pages')->save($page);
return $this->wire()->pages->save($page, array('adjustName' => false));
}
/**
@@ -453,6 +457,10 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
*
* If you attempt to delete a page with children, and dont specifically set the `$recursive` argument to `true`, then
* this method will throw an exception. If a recursive delete fails for any reason, an exception will be thrown.
*
* Hook note:
* If you want to hook this method, please hook the `deleteReady`, `deleted`, or `Pages::delete` method
* instead, as hooking this method will not hook relevant pages deleted directly through $pages->delete().
*
* @param Page $page
* @param bool $recursive If set to true, then this will attempt to delete all children too.
@@ -471,6 +479,10 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
* - If the page has any other fields, they will not be populated, only the name will.
* - Returns a `NullPage` on error, such as when a page of this type already exists with the same name/parent.
*
* Hook note:
* If you want to hook this method, please hook the `addReady`, `Pages::add`, or `Pages::addReady` method
* instead, as hooking this method will not hook relevant pages added directly through $pages->add().
*
* @param string $name Name to use for the new page
* @return Page|NullPage
*
@@ -479,7 +491,7 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
$parent = $this->getParent();
$page = $this->wire('pages')->newPage(array(
$page = $this->wire()->pages->newPage(array(
'pageClass' => $this->getPageClass(),
'template' => $this->template
));
@@ -492,7 +504,7 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
} catch(\Exception $e) {
$this->trackException($e, false);
$page = $this->wire('pages')->newNullPage();
$page = $this->wire()->pages->newNullPage();
}
return $page;
@@ -570,7 +582,7 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
*
*/
public function getParent() {
return $this->wire('pages')->get($this->parent_id);
return $this->wire()->pages->get($this->parent_id);
}
/**
@@ -583,10 +595,10 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
*/
public function getParents() {
if(count($this->parents)) {
return $this->wire('pages')->getById($this->parents);
return $this->wire()->pages->getById($this->parents);
} else {
$parent = $this->getParent();
$parents = $this->wire('pages')->newPageArray();
$parents = $this->wire()->pages->newPageArray();
$parents->add($parent);
return $parents;
}
@@ -642,7 +654,7 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
$selectorString = $this->selectorString($selectorString);
$defaults = array('findAll' => true);
$options = array_merge($defaults, $options);
return $this->wire('pages')->count($selectorString, $options);
return $this->wire()->pages->count($selectorString, $options);
}
/**
@@ -671,7 +683,6 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
*
*/
public function ___saveReady(Page $page) {
if($page) {}
return array();
}

View File

@@ -13,7 +13,7 @@
* #pw-body
* #pw-summary-manipulation In most cases you will not need these manipulation methods as core API calls already take care of this.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @method string renderPager(array $options = array()) Renders pagination, when MarkupPageArray module installed
@@ -79,7 +79,9 @@ class PaginatedArray extends WireArray implements WirePaginatable {
*
*/
public function getTotal() {
return $this->numTotal;
$total = $this->numTotal;
if(!$total) $total = $this->count();
return $total;
}
/**
@@ -327,4 +329,4 @@ class PaginatedArray extends WireArray implements WirePaginatable {
}
return $info;
}
}
}

View File

@@ -5,7 +5,7 @@
* Class to hold combined password/salt info. Uses Blowfish when possible.
* Specially used by FieldtypePassword.
*
* ProcessWire 3.x, Copyright 2019 by Ryan Cramer
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
* @method setPass($value) Protected internal use method
@@ -24,7 +24,7 @@ class Password extends Wire {
protected $data = array(
'salt' => '',
'hash' => '',
);
);
/**
* @var WireRandom|null
@@ -56,8 +56,12 @@ class Password extends Wire {
}
if(strlen($hash) < 29) return false;
$matches = ($hash === $this->data['hash']);
if(function_exists("\\hash_equals")) {
$matches = hash_equals($this->data['hash'], $hash);
} else {
$matches = ($hash === $this->data['hash']);
}
if($matches && $updateNotify) {
$this->message($this->_('The password system has recently been updated. Please change your password to complete the update for your account.'));
@@ -71,13 +75,13 @@ class Password extends Wire {
*
* #pw-group-internal
*
* @param string $key
* @param string $name
* @return mixed
*
*/
public function __get($key) {
if($key == 'salt' && !$this->data['salt']) $this->data['salt'] = $this->salt();
return isset($this->data[$key]) ? $this->data[$key] : null;
public function __get($name) {
if($name === 'salt' && empty($this->data['salt'])) $this->data['salt'] = $this->salt();
return isset($this->data[$name]) ? $this->data[$name] : null;
}
/**
@@ -91,7 +95,7 @@ class Password extends Wire {
*/
public function __set($key, $value) {
if($key == 'pass') {
if($key === 'pass') {
// setting the password
$this->setPass($value);
@@ -216,6 +220,8 @@ class Password extends Wire {
*
*/
protected function hash($pass) {
$config = $this->wire()->config;
// if there is no salt yet, make one (for new pass or reset pass)
if(strlen($this->data['salt']) < 28) $this->data['salt'] = $this->salt();
@@ -227,10 +233,10 @@ class Password extends Wire {
$salt1 = $this->data['salt'];
// static salt stored in config.php
$salt2 = (string) $this->wire('config')->userAuthSalt;
$salt2 = (string) $config->userAuthSalt;
// auto-detect the hash type based on the format of the salt
$hashType = $this->isBlowfish($salt1) ? 'blowfish' : $this->wire('config')->userAuthHashType;
$hashType = $this->isBlowfish($salt1) ? 'blowfish' : $config->userAuthHashType;
if(!$hashType) {
// If there is no defined hash type, and the system doesn't support blowfish, then just use md5 (ancient backwards compatibility)
@@ -356,4 +362,3 @@ class Password extends Wire {
}
}

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